├── .gitignore
├── LICENSE
├── README.md
├── doc
└── screenshots
│ ├── bin_vheap_delta_flame.png
│ ├── bin_vheap_delta_heat.png
│ ├── bin_vheap_flame.png
│ ├── bin_vheap_heat.png
│ ├── mbuf_size_delta_heat.png
│ ├── reductions_delta_flame.png
│ ├── reductions_delta_heat.png
│ └── samples_heat.png
├── rebar.config
└── src
├── flame_prof.app.src
├── flame_prof.erl
└── seltor@flame_prof.erl
/.gitignore:
--------------------------------------------------------------------------------
1 | ##
2 | ## Copyright 2020 Medical-Objects Pty Ltd.
3 | ##
4 | ## Licensed under the Apache License, Version 2.0 (the "License");
5 | ## you may not use this file except in compliance with the License.
6 | ## You may obtain a copy of the License at
7 | ##
8 | ## http://www.apache.org/licenses/LICENSE-2.0
9 | ##
10 | ## Unless required by applicable law or agreed to in writing, software
11 | ## distributed under the License is distributed on an "AS IS" BASIS,
12 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | ## See the License for the specific language governing permissions and
14 | ## limitations under the License.
15 | ##
16 |
17 | ## Rebar 3
18 | .rebar3
19 | _*
20 | .eunit
21 | *.o
22 | *.beam
23 | *.plt
24 | *.swp
25 | *.swo
26 | .erlang.cookie
27 | ebin
28 | log
29 | erl_crash.dump
30 | .rebar
31 | logs
32 | _build
33 | .idea
34 | *.iml
35 | rebar3.crashdump
36 | .vscode
37 | ## Custom
38 | /tmp
39 | .vscode
40 | /samples
41 | /doc/*
42 | !/doc/screenshots
43 | rebar.lock
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Medical-Objects Pty Ltd.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # **flame_prof**
2 |
3 |
NOTE: This project is under active development. Reference documentation,
4 | tutorials and a number features are in the works.
5 |
6 |
7 | ## Contents
8 |
9 | - [Overview](#Overview)
10 | - [Screenshots](#Screenshots)
11 | - [Example](#Example)
12 | - [Samples](#Samples)
13 | - [High scheduler load](#High-scheduler-load)
14 | - [Off-heap binaries](#Off-heap-binaries)
15 | - [There's much more](#Theres-much-more)
16 | - [Build](#Build)
17 |
18 |
19 | ## Overview
20 |
21 | **flame_prof** is a general-purpose Erlang profiler a little like OTP's
22 | **fprof**, except ...
23 |
24 | + It generates Linux [perf_events](https://en.wikipedia.org/wiki/Perf_(Linux))
25 | script [output](https://linux.die.net/man/1/perf-script) (even on Win/macOS)
26 | intended to be consumed by, and analysed with, [a fork of Netflix's Flamescope](https://github.com/ebegumisa/flamescope).
27 |
28 | + It uses a call-stack sampling approach rather than attempting to measure each
29 | individual call. So it does not _need_ to use Erlang tracing (though it _may_
30 | use tracing for certain optional features).
31 |
32 | + It retains calling process information including process status, memory usage,
33 | message queue lengths and garbage collection info.
34 |
35 | + Provides control over output file writing (e.g. sample flush frequency, output
36 | file rotation).
37 |
38 | + It provides means to automatically select processes to be profiled
39 | (e.g. top 100 by reductions). Automatically triggering profiling in controlled
40 | manner is coming soon.
41 |
42 | ## Screenshots
43 |
44 | ### Example
45 |
46 | Below are a few (of many) sub-second heatmaps and Erlang code flamegraphs
47 | generated from a node being profiled once for a little over a minute. The
48 | profiler was configured to, every 5 seconds, auto-select and profile the top 100
49 | processes.
50 |
51 | ### Samples
52 |
53 | Colour saturation in the heatmap below is scaled to the number of callstack
54 | samples taken by the profiler. Grey areas show where the node was so busy, the
55 | profiler was unable to take regular samples (long lines of grey are a special
56 | case -- these are due to the profiler flushing samples to disk every 15
57 | seconds.)
58 |
59 | Despite the load, there are still plenty of samples in the middle busy part to
60 | be able to gain insights from the other heatmaps and flamegraphs in the sections
61 | that follow. Moreover because the other heatmaps and flamegraphs integrate
62 | metrics mesurements like reductions differences, even large gaps between samples
63 | still gives us usable data.
64 |
65 | 
66 |
67 | ### High scheduler load
68 |
69 | The reductions heatmap and corresponding flamegraph below show what callstacks
70 | contributed most to schedular load in the example profile. Colour saturation in
71 | the heatmap and widths in the flamegraph are scaled to reductions. _Scaling
72 | flamegraph widths by metrics measured during sampling_ is the first of
73 | **flame_prof's two main innovations**.
74 |
75 | Crucially, in the flamegraph, the callstacks are categorised by the _process
76 | status_ of upto 100 processes running that particular code (the example profile
77 | auto-selected the top 100.) Such _callstack categorisation/grouping_ is
78 | **flame_prof's other main innovation**. It has proven extremely useful
79 | especially in identifying code causing concurrency bottlenecks or memory issues.
80 |
81 | 
82 |
83 | 
84 |
85 | ### Off-heap binaries
86 |
87 | In addition to CPU load, **flame_prof** can also be used to identify code
88 | causing memory and/or garbage collection problems.
89 |
90 | Take binaries for instance. The bin_vheap_size heatmaps and corresponding
91 | flamegraphs below show size of off-heap binaries referenced by the callstacks.
92 | Colour saturation in the heatmap and widths in the flamegraph are scaled to
93 | words.
94 |
95 | Using some more callstack categorisation, the flamegraph is able to further
96 | group callstacks by the memory used by those processes when the calls were being
97 | made.
98 |
99 | 
100 |
101 | 
102 |
103 | Below, we again see bin_vheap_size heatmaps and flamegraphs but this time by the
104 | delta. Green hue in the heatmap shows releases with saturation scaled to words.
105 | The flamegraph is further still categorised by positive (references) and
106 | negative (releases), the width of both scaled to words.
107 |
108 | 
109 |
110 | 
111 |
112 | ## There's much more
113 |
114 | In addition to above, **flame_prof** generates heatmaps and flamegraphs for
115 | process message queue lengths, message buffer sizes, stack memory, heap memory
116 | and garbage collection (heap block sizes, etc) allowing the developer to easily
117 | and meaningfully cross reference these metrics with thousands of callstacks
118 | sampled from thousands of processes over a timeline.
119 |
120 | ## Build
121 |
122 | $ rebar3 compile
--------------------------------------------------------------------------------
/doc/screenshots/bin_vheap_delta_flame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/bin_vheap_delta_flame.png
--------------------------------------------------------------------------------
/doc/screenshots/bin_vheap_delta_heat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/bin_vheap_delta_heat.png
--------------------------------------------------------------------------------
/doc/screenshots/bin_vheap_flame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/bin_vheap_flame.png
--------------------------------------------------------------------------------
/doc/screenshots/bin_vheap_heat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/bin_vheap_heat.png
--------------------------------------------------------------------------------
/doc/screenshots/mbuf_size_delta_heat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/mbuf_size_delta_heat.png
--------------------------------------------------------------------------------
/doc/screenshots/reductions_delta_flame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/reductions_delta_flame.png
--------------------------------------------------------------------------------
/doc/screenshots/reductions_delta_heat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/reductions_delta_heat.png
--------------------------------------------------------------------------------
/doc/screenshots/samples_heat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebegumisa/flame_prof/8532cf07e1de1b29e8f6350da847cebab8eccbfc/doc/screenshots/samples_heat.png
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %%
2 | %% Copyright 2020 Medical-Objects Pty Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%
16 |
17 | {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard]}.
--------------------------------------------------------------------------------
/src/flame_prof.app.src:
--------------------------------------------------------------------------------
1 | %%
2 | %% Copyright 2020 Medical-Objects Pty Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%
16 |
17 | {application, flame_prof,
18 | [{description, "Heatmap and flamegraph sampling profiler for Erlang"},
19 | {vsn, "0.1.0"},
20 | {applications, [kernel,stdlib]},
21 | {modules, []},
22 | {maintainers, ["Edmond Begumisa"]},
23 | {licenses, ["Apache 2.0"]}
24 | ]}.
25 |
--------------------------------------------------------------------------------
/src/flame_prof.erl:
--------------------------------------------------------------------------------
1 | %%
2 | %% Copyright 2020 Medical-Objects Pty Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%
16 |
17 | %%% ============================================================================
18 | %%% @doc Erlang profiler which generates Linux perf_events output (even on
19 | %%% Windows/MacOS).
20 | %%%
21 | %%% This is a general-purpose erlang profiler a little like OTP's `fprof',
22 | %%% except...
23 | %%%
24 | %%% It generates Linux perf_events [1] script output [2] (even on
25 | %%% Win/macOS) intended to be consumed by, and analysed with, a fork of
26 | %%% Netflix's Flamescope [3].
27 | %%% It uses a call-stack sampling approach rather than attempting to
28 | %%% measure each individual call. So it does not need to use Erlang
29 | %%% tracing (though it may use tracing for certain optional features).
30 | %%%
31 | %%% It retains calling process information including process status,
32 | %%% memory usage, message queue lengths and garbage collection info.
33 | %%%
34 | %%% Provides control over output file writing (e.g. sample flush
35 | %%% frequency, output file rotation).
36 | %%% It provides means to automatically select processes to be profiled
37 | %%% (e.g. top 100 by reductions). Automatically triggering profiling in
38 | %%% in a controlled manner is coming soon.
39 | %%%
40 | %%% [1] [https://en.wikipedia.org/wiki/Perf_(Linux)]
41 | %%% [2] [https://linux.die.net/man/1/perf-script]
42 | %%% [3] [https://github.com/ebegumisa/flamescope]
43 | %%% @end
44 | %%% ============================================================================
45 | -module(flame_prof).
46 |
47 | %%% -- Public Control --
48 | -export([start/0, start/1, start/2,
49 | start_link/0, start_link/1, start_link/2,
50 | stop/0, stop/1, stop/2]).
51 |
52 | %%% -- Public Interface --
53 | -export([apply/1, apply/2,
54 | profile_start/0, profile_start/1,
55 | profile_stop/0]).
56 | -type process_status_t() ::
57 | exiting | garbage_collecting | waiting | running | runnable | suspended.
58 | -type viral_mode_t() ::
59 | true | false | spawn | spawn_and_send | send | self_only.
60 | -type process_opts_t() ::
61 | #{viral => undefined | viral_mode_t(),
62 | status => undefined | all | process_status_t()}.
63 | -type start_opts_t() ::
64 | #{basename => undefined | file:filename(),
65 | flatten_recursive => undefined | boolean(),
66 | sample_interval => undefined | pos_integer(),
67 | flush_interval => undefined | pos_integer(),
68 | flush_close_max => undefined | infinity | pos_integer(),
69 | files_max => undefined | infinity | pos_integer(),
70 | auto_select =>
71 | undefined
72 | | {top,ProcessCount :: pos_integer()}
73 | | {top,ProcessCount :: pos_integer(),Interval :: pos_integer()},
74 | auto_process_opts => process_opts_t()}.
75 | -export_type([start_opts_t/0,
76 | process_status_t/0, viral_mode_t/0,
77 | process_opts_t/0]).
78 |
79 | %%% -- Private Interface --
80 | -export([sampler/3]).
81 | -export([system_continue/3, system_terminate/4, system_get_state/1]).
82 |
83 | -type perf_counter_t() :: integer().
84 | -type millisecs_t() :: non_neg_integer().
85 | -type stack_initial_item_t() ::
86 | {Mod :: module(), Func :: atom(), Arity :: non_neg_integer()}.
87 | -type stack_item_t() ::
88 | {Mod :: module(), Func :: atom(), Arity :: non_neg_integer(),
89 | [{file,string()} | {line,non_neg_integer()}]}.
90 | -type stack_t() :: [stack_initial_item_t() | stack_item_t()].
91 | -type stacks_t() :: #{pid() => stack_t()}.
92 | -type process_metrics_t() ::
93 | [{atom(),pos_integer()} | {atom(),[{atom(),pos_integer()}]}].
94 | -type process_metrics_delta_t() ::
95 | [{atom(),pos_integer(),integer()} |
96 | {atom(),[{atom(),pos_integer(),integer()}]}].
97 | -type processes_t() ::
98 | #{pid() =>
99 | {StatusFilter :: all | process_status_t(),
100 | Metrics :: undefined | process_metrics_t()}}.
101 | -type process_grp_t() :: pos_integer().
102 | -type sample_t() :: {TS :: perf_counter_t(),
103 | MetricsDelta :: process_metrics_delta_t(),
104 | Stacks :: stacks_t(),
105 | InitialCalls :: [stack_initial_item_t()]}.
106 | -type reg_names_t() :: #{pid() => 0 | atom()}.
107 |
108 | %%% -- Private Preprocess --
109 | -define(DFLT_SAMPLE_INTERVAL, 9). % 99 hz (combined for all processes)
110 | -define(DFLT_FLUSH_INTERVAL, 15000). % 15 secs
111 | -define(DFLT_FLUSH_CLOSE_MAX, 8). % 2 mins worth
112 | -define(DFLT_FILES_MAX, 15). % 30 mins worth (30 / 2).
113 | -define(DFLT_TOP_INTERVAL, 10000). % 10 secs (etop's default).
114 |
115 | -define(DELAY_SIZE, 10*1024*1024). % 10 MiB
116 | -define(DELAY_INTERVAL, 5*60*1000). % 5 mins
117 |
118 | -define(BACKTRACE_DEPTH, 255).
119 |
120 | -record(st_r,
121 | {flush_tout :: perf_counter_t(),
122 | flush_close_max :: non_neg_integer(),
123 | flush_count=0 :: non_neg_integer(),
124 | sample_tout :: perf_counter_t(),
125 | sample_tout_ms :: millisecs_t(),
126 | prev_sample_ts :: undefined | perf_counter_t(),
127 | prev_flush_ts :: undefined | perf_counter_t(),
128 | ps_grps=#{} :: #{process_grp_t() => processes_t()},
129 | expl_ps=#{} :: #{pid() => process_grp_t()},
130 | auto_ps=#{} :: #{pid() => process_grp_t()},
131 | auto_ts :: undefined | perf_counter_t(),
132 | reg_names=#{} :: reg_names_t(),
133 | samples=[] :: [sample_t()],
134 | file :: undefined | file:io_device(),
135 | paths :: undefined | {non_neg_integer(),queue:queue(file:filename())},
136 | auto_selector :: undefined | pid(),
137 | auto_status :: process_status_t(),
138 | auto_traceopts :: [set_on_spawn|send]}).
139 |
140 | -include_lib("kernel/include/file.hrl").
141 |
142 | %%% -- Control -----------------------------------------------------------------
143 |
144 | -spec start() -> pid() | {error,{already_started,pid()}}.
145 |
146 | start() ->
147 | start(#{}).
148 |
149 |
150 | -spec start(start_opts_t() | non_neg_integer() | infinity) ->
151 | pid() | {error,{already_started,pid()}}.
152 |
153 | start(#{}=Opts) ->
154 | do_start(false, Opts, 5000);
155 | start(TOut) ->
156 | do_start(false, #{}, TOut).
157 |
158 |
159 | -spec start(start_opts_t(), non_neg_integer() | infinity) ->
160 | pid() | {error,{already_started,pid()}}.
161 |
162 | start(#{}=Opts, TOut) ->
163 | do_start(false, Opts, TOut).
164 |
165 |
166 | -spec start_link() -> pid() | {error,{already_started,pid()}}.
167 |
168 | start_link() ->
169 | start_link(#{}).
170 |
171 |
172 | -spec start_link(start_opts_t() | non_neg_integer() | infinity) ->
173 | pid() | {error,{already_started,pid()}}.
174 |
175 | start_link(#{}=Opts) ->
176 | do_start(true, Opts, 5000);
177 | start_link(TOut) ->
178 | do_start(true, #{}, TOut).
179 |
180 |
181 | -spec start_link(start_opts_t(), non_neg_integer() | infinity) ->
182 | pid() | {error,{already_started,pid()}}.
183 |
184 | start_link(#{}=Opts, TOut) ->
185 | do_start(true, Opts, TOut).
186 |
187 |
188 | do_start(ShouldLnk, #{}=Opts, TOut)
189 | when is_boolean(ShouldLnk), is_integer(TOut) orelse TOut=:=infinity
190 | ->
191 | A = [self(),chk_start_opts(Opts),TOut],
192 | case whereis(?MODULE) of
193 | undefined when ShouldLnk ->
194 | proc_lib:start_link(?MODULE, sampler, A, TOut, [{priority,max}]);
195 | undefined ->
196 | proc_lib:start(?MODULE, sampler, A, TOut, [{priority,max}]);
197 | P ->
198 | {error,{already_started,P}}
199 | end.
200 |
201 |
202 | stop() ->
203 | stop(?MODULE).
204 |
205 | stop(TOut) when is_integer(TOut); TOut=:=infinity ->
206 | stop(?MODULE, TOut);
207 | stop(P) ->
208 | stop(P, 5000).
209 |
210 | stop(P, TOut) when is_pid(P) orelse is_atom(P),
211 | is_integer(TOut) orelse TOut=:=infinity
212 | ->
213 | try proc_lib:stop(P, normal, TOut)
214 | catch exit:noproc -> ok
215 | end.
216 |
217 |
218 | %%% -- Interface ---------------------------------------------------------------
219 |
220 | -spec apply(fun(() -> term())) -> term().
221 | %%
222 | %% @doc Same as calling {@link apply/2} with empty profiling process options
223 | %% (i.e. default process options).
224 | %%
225 | apply(Fn) ->
226 | ?MODULE:apply(Fn, #{}).
227 |
228 |
229 | -spec apply(fun(() -> term()), process_opts_t()) -> term().
230 | %%
231 | %% @doc Profile the evaluation of a given function in the context of the calling
232 | %% process using the given profiling process options.
233 | %%
234 | apply(Fn, Opts) ->
235 | profile_start(Opts),
236 | try Fn()
237 | after profile_stop()
238 | end.
239 |
240 |
241 | -spec profile_start() -> non_neg_integer().
242 | %%
243 | %% @doc Same as calling {@link profile_start/1} with emtpy profiling process
244 | %% options (i.e. default process options).
245 | %%
246 | profile_start() ->
247 | profile_start(#{}).
248 |
249 |
250 | -spec profile_start(process_opts_t()) -> non_neg_integer().
251 | %%
252 | %% @doc Start profiling the calling process using the given profiling process
253 | %% options.
254 | %%
255 | profile_start(Opts) ->
256 | profile_start1(chk_p_opts(Opts, spawn)).
257 |
258 | profile_start1(#{viral:=Viral,status:=Status}) ->
259 | Sampler = whereis(?MODULE),
260 | true = is_pid(Sampler) orelse error({not_started,?MODULE}),
261 | ViralTraceOpts = viral_to_traceopts(Viral),
262 | % Notify sampler about this process by briefly tracing a single call to
263 | % explicit/1.
264 | erlang:trace_pattern({?MODULE,explicit,1}, true, [local]),
265 | erlang:trace(self(), true, [{tracer,Sampler},'call']),
266 | try Status = explicit(Status)
267 | after
268 | erlang:trace(self(), false, [all]),
269 | erlang:trace_pattern({?MODULE,process,1}, false, [local])
270 | end,
271 | % Now that the sampler knows to sample us, profile without tracing any
272 | % calls.
273 | erlang:system_flag(backtrace_depth, ?BACKTRACE_DEPTH),
274 | maybe_trace_send_on(ViralTraceOpts),
275 | trace_p_on(self(), Sampler, ViralTraceOpts).
276 |
277 |
278 | -spec profile_stop() -> non_neg_integer().
279 |
280 | profile_stop() ->
281 | trace_p_off(self()).
282 |
283 | %%% -- sampler proc_lib impl --
284 |
285 | explicit(Status) -> Status.
286 |
287 | sampler(Parent,
288 | #{sample_interval:=SampleMs,flush_interval:=FlushMs,
289 | flush_close_max:=CloseMax,auto_select:=AutoSel,
290 | auto_process_opts:=#{status:=AutoStatus,viral:=AutoViral}}=Opts,
291 | TOut) when is_pid(Parent)
292 | ->
293 | process_flag(trap_exit, true),
294 | try register(?MODULE, self()) of
295 | true ->
296 | S = #st_r{sample_tout_ms=SampleMs,
297 | sample_tout=pc_from_millisecs(SampleMs),
298 | flush_tout=pc_from_millisecs(FlushMs),
299 | flush_close_max=CloseMax,
300 | paths=del_excess_files(Opts),
301 | auto_status=AutoStatus,
302 | auto_traceopts=viral_to_traceopts(AutoViral),
303 | auto_selector=
304 | case undefined=:=AutoSel
305 | orelse
306 | seltor@flame_prof:start_link(
307 | AutoSel, [{timeout,TOut}])
308 | of
309 | {ok,P} -> P;
310 | true -> undefined
311 | end},
312 | ok = proc_lib:init_ack(Parent, {ok,self()}),
313 | sampler_loop(Parent, nil, S, Opts)
314 | catch error:badarg ->
315 | {error,{already_started,erlang:whereis(?MODULE)}}
316 | end.
317 |
318 | sampler_loop(Parent, nil, #st_r{}=S, Opts)
319 | when map_size(S#st_r.expl_ps)=:=0, map_size(S#st_r.auto_ps)=:=0
320 | ->
321 | % Nothing to sample as yet, so wait here until there is.
322 | receive Msg -> sampler_loop(Parent, Msg, S, Opts) end;
323 | sampler_loop(Parent, nil, #st_r{}=S0, Opts) ->
324 | % Figure out if it's time to sample and/or flush the stacks.
325 | TS = os:perf_counter(),
326 | S1 = sampler_maybe_sample(TS, S0, Opts),
327 | % Wait for more profile events, otherwise check if we need to sample roughly
328 | % after the sample interval.
329 | receive Msg -> sampler_loop(Parent, Msg, S1, Opts)
330 | after S1#st_r.sample_tout_ms -> sampler_loop(Parent, nil, S1, Opts)
331 | end;
332 | sampler_loop(Parent, {auto_selected,[_|_]=Ps}, #st_r{}=S, Opts) ->
333 | % seltor@flame_prof has sent the set of processes to automatically
334 | % profile. Replace the ones we have.
335 | TS = os:perf_counter(),
336 | erlang:system_flag(backtrace_depth, ?BACKTRACE_DEPTH),
337 | maybe_trace_send_on(S#st_r.auto_traceopts),
338 | sampler_loop(
339 | Parent, nil, sampler_ps_add_auto(Ps, S#st_r{auto_ts=TS}), Opts);
340 | sampler_loop(Parent, {trace,P,call,{?MODULE,explicit,[Status]}},
341 | #st_r{}=S, Opts)
342 | ->
343 | % Add calling process to map of processes to explicitly sample
344 | % (call is as result of an external call to apply/N or profile_start/N).
345 | sampler_loop(Parent, nil, sampler_p_add_expl(P, Status, S), Opts);
346 | sampler_loop(Parent, {trace,_,call,_}, #st_r{}=S, Opts) ->
347 | sampler_loop(Parent, nil, S, Opts);
348 | sampler_loop(Parent, {trace,_,send,_,_}, #st_r{}=S, Opts)
349 | when map_size(S#st_r.expl_ps)=:=0, map_size(S#st_r.auto_ps)=:=0
350 | ->
351 | trace_send_off(),
352 | sampler_loop(Parent, nil, S, Opts);
353 | sampler_loop(Parent, {trace,_,send,_,{NmeOrP,Nd}}=Ev, #st_r{}=S, Opts) ->
354 | case Nd=:=node() andalso (is_pid(NmeOrP) orelse whereis(NmeOrP)) of
355 | true ->
356 | sampler_loop(Parent, setelement(5, Ev, NmeOrP), S, Opts);
357 | false ->
358 | sampler_loop(Parent, nil, S, Opts);
359 | undefined ->
360 | sampler_loop(Parent, nil, S, Opts);
361 | P when is_pid(P) ->
362 | sampler_loop(Parent, setelement(5, Ev, P), S, Opts)
363 | end;
364 | sampler_loop(Parent, {trace,Frm,send,_,To}, #st_r{}=S0, Opts)
365 | when is_pid(Frm), is_pid(To)
366 | ->
367 | % Add local recipient process to map of processes to explicitly sample
368 | % if the sending process is being explicitly sampled (i.e. send is as result
369 | % of an exernal call to apply/N or profile_start/N)
370 | S1 = case node(To)=:=node() andalso S0#st_r.expl_ps of
371 | #{Frm:=ExplGrp} ->
372 | case S0#st_r.ps_grps of
373 | #{ExplGrp:=#{Frm:=ExplStatus}} ->
374 | sampler_p_add_expl(To, ExplStatus, S0);
375 | #{} ->
376 | local
377 | end;
378 | #{} ->
379 | local;
380 | false ->
381 | remote
382 | end,
383 | S2 = case S1=:=local andalso S1#st_r.auto_ps of
384 | #{Frm:=AutoGrp} ->
385 | % Add local recipient process to map of processes to auto sample
386 | % if sending process is being auto sampled.
387 | case S1#st_r.ps_grps of
388 | #{AutoGrp:=#{Frm:=AutoStatus}} ->
389 | sampler_p_add_auto(To, AutoStatus, S1);
390 | #{} ->
391 | S0
392 | end;
393 | #{} ->
394 | S1;
395 | false ->
396 | S0
397 | end,
398 | sampler_loop(Parent, nil, S2, Opts);
399 | sampler_loop(Parent, {trace,Frm,send,_,_}=Ev, #st_r{}=S, Opts) when is_atom(Frm)
400 | ->
401 | case whereis(Frm) of
402 | undefined ->
403 | sampler_loop(Parent, nil, S, Opts);
404 | P ->
405 | sampler_loop(Parent, setelement(2, Ev, P), S, Opts)
406 | end;
407 | sampler_loop(Parent, {trace,_,send,_,To}=Ev, #st_r{}=S, Opts) when is_atom(To) ->
408 | case whereis(To) of
409 | undefined ->
410 | sampler_loop(Parent, nil, S, Opts);
411 | P ->
412 | sampler_loop(Parent, setelement(5, Ev, P), S, Opts)
413 | end;
414 | sampler_loop(Parent, {trace,_,send,_,To}, #st_r{}=S, Opts) when is_port(To) ->
415 | sampler_loop(Parent, nil, S, Opts);
416 | sampler_loop(Parent, {trace,_,send_to_non_existing_process,_,_}, #st_r{}=S,
417 | Opts)
418 | ->
419 | map_size(S#st_r.expl_ps)=:=0 andalso map_size(S#st_r.auto_ps)=:=0 andalso
420 | trace_send_off(),
421 | sampler_loop(Parent, nil, S, Opts);
422 | sampler_loop(Parent, {trace,P,exit,_}, #st_r{}=S, Opts) ->
423 | sampler_loop(Parent, nil, sampler_p_remove(P, S), Opts);
424 | sampler_loop(Parent, {trace,P,register,Nme}, #st_r{reg_names=Nmes}=S, Opts) ->
425 | sampler_loop(Parent, nil, S#st_r{reg_names=Nmes#{P=>Nme}}, Opts);
426 | sampler_loop(Parent, {trace,P,unregister,_}, #st_r{reg_names=Nmes}=S, Opts) ->
427 | sampler_loop(Parent, nil, S#st_r{reg_names=Nmes#{P=>0}}, Opts);
428 | sampler_loop(Parent, {trace,_,spawn,_,_}, #st_r{}=S, Opts) ->
429 | sampler_loop(Parent, nil, S, Opts);
430 | sampler_loop(Parent, {trace,_,spawned,_,_}, #st_r{}=S, Opts) ->
431 | sampler_loop(Parent, nil, S, Opts);
432 | sampler_loop(Parent, {trace,_,link,_}, #st_r{}=S, Opts) ->
433 | sampler_loop(Parent, nil, S, Opts);
434 | sampler_loop(Parent, {trace,_,unlink,_}, #st_r{}=S, Opts) ->
435 | sampler_loop(Parent, nil, S, Opts);
436 | sampler_loop(Parent, {trace,_,getting_linked,_}, #st_r{}=S, Opts) ->
437 | sampler_loop(Parent, nil, S, Opts);
438 | sampler_loop(Parent, {trace,_,getting_unlinked,_}, #st_r{}=S, Opts) ->
439 | sampler_loop(Parent, nil, S, Opts);
440 | sampler_loop(Parent, Other, #st_r{auto_selector=AutoSeltor}=S, Opts) ->
441 | % Handle 'EXIT' messages, sys messages and unexpected trace events.
442 | case Other of
443 | {'EXIT',Parent,Reason} ->
444 | system_terminate(Reason, Parent, undefined, {S,Opts});
445 | {'EXIT',AutoSeltor,Reason} ->
446 | case is_terminate_crash(Reason) of
447 | true -> system_terminate(Reason, Parent, undefined, {S,Opts});
448 | false -> sampler_loop(Parent, nil, S, Opts)
449 | end;
450 | {system,Frm,Rq} ->
451 | sys:handle_system_msg(
452 | Rq, Frm, Parent, ?MODULE, undefined, {S,Opts});
453 | Other when Other=/=nil ->
454 | system_terminate({unexpected,Other}, Parent, undefined, {S,Opts})
455 | end.
456 |
457 | sampler_p_remove(P, #st_r{ps_grps=PsGrps}=S) ->
458 | case S#st_r.expl_ps of
459 | #{P:=Grp} ->
460 | #{Grp:=Ps} = PsGrps,
461 | false = maps:is_key(P, S#st_r.auto_ps),
462 | S#st_r{ps_grps=PsGrps#{Grp:=maps:remove(P, Ps)},
463 | expl_ps=maps:remove(P, S#st_r.expl_ps),
464 | reg_names=maps:remove(P, S#st_r.reg_names)};
465 | #{} ->
466 | case S#st_r.auto_ps of
467 | #{P:=Grp} ->
468 | #{Grp:=Ps} = PsGrps,
469 | false = maps:is_key(P, S#st_r.expl_ps),
470 | S#st_r{ps_grps=PsGrps#{Grp:=maps:remove(P, Ps)},
471 | reg_names=maps:remove(P, S#st_r.reg_names)};
472 | #{} ->
473 | false = maps:is_key(P, S#st_r.reg_names),
474 | S
475 | end
476 | end.
477 |
478 | sampler_p_add_grp(P, Status, PsGrps, Ps) ->
479 | Grp = case maps:is_key(P, Ps) of
480 | true -> maps:get(P, Ps);
481 | false -> rand:uniform(11)
482 | end,
483 | {case PsGrps of
484 | #{Grp:=#{P:=X}=PsGrp} ->
485 | {_,Metrics} = X,
486 | PsGrps#{Grp:=PsGrp#{P=>{Status,Metrics}}};
487 | #{Grp:=PsGrp} ->
488 | PsGrps#{Grp:=PsGrp#{P=>{Status,undefined}}};
489 | #{} ->
490 | PsGrps#{Grp=>#{P=>{Status,undefined}}}
491 | end,
492 | Ps#{P=>Grp}}.
493 |
494 | sampler_p_add_expl(P, Status, S) ->
495 | {PsGrps,ExplPs} =
496 | sampler_p_add_grp(P, Status, S#st_r.ps_grps, S#st_r.expl_ps),
497 | S#st_r{ps_grps=PsGrps,
498 | expl_ps=ExplPs,
499 | auto_ps=maps:remove(P, S#st_r.auto_ps)}.
500 |
501 | sampler_p_add_auto(P, Status, S) ->
502 | case maps:is_key(P, S#st_r.expl_ps) of
503 | false ->
504 | {PsGrps,AutoPs} =
505 | sampler_p_add_grp(P, Status, S#st_r.ps_grps, S#st_r.auto_ps),
506 | S#st_r{ps_grps=PsGrps,auto_ps=AutoPs};
507 | true ->
508 | S
509 | end.
510 |
511 | sampler_ps_add_auto(Ps, S) ->
512 | sampler_ps_add_auto1(Ps, S#st_r.auto_ps, [], S).
513 |
514 | sampler_ps_add_auto1([H|T], OrigAutoPs, AccPs, S) ->
515 | case S of
516 | #st_r{expl_ps=#{H:=_}} ->
517 | sampler_ps_add_auto1(T, OrigAutoPs, AccPs, S);
518 | #st_r{auto_status=AutoStatus} ->
519 | % Ensure we're tracing proccess if it's to be virally profiled.
520 | S#st_r.auto_traceopts=/=[] andalso
521 | trace_p_on(H, self(), S#st_r.auto_traceopts),
522 | sampler_ps_add_auto1(
523 | T, OrigAutoPs, [H|AccPs], sampler_p_add_auto(H, AutoStatus, S))
524 | end;
525 | sampler_ps_add_auto1([], #{}=OrigAutoPs, [_|_]=AccPs, S0) ->
526 | if
527 | map_size(OrigAutoPs)=:=0 ->
528 | #st_r{}=S0;
529 | true ->
530 | % Stop profiling processes that were, but are no longer, in auto
531 | % profiled set.
532 | maps:fold(
533 | fun(P, _, S1) ->
534 | S0#st_r.auto_traceopts=/=[] andalso
535 | trace_p_off(P), % Stop tracing virally profiled process
536 | sampler_p_remove(P, S1)
537 | end, S0, maps:without(AccPs, OrigAutoPs))
538 | end;
539 | sampler_ps_add_auto1([], #{}, [], S) ->
540 | S.
541 |
542 | sampler_maybe_sample(TS, #st_r{prev_flush_ts=undefined}=S, _) ->
543 | % First loop
544 | S#st_r{prev_sample_ts=TS,prev_flush_ts=TS};
545 | sampler_maybe_sample(TS, S, Opts) ->
546 | if
547 | TS - S#st_r.prev_flush_ts >= S#st_r.flush_tout ->
548 | % Sample once more, flush all samples then possibly close.
549 | FileS =
550 | if
551 | S#st_r.file=:=undefined ->
552 | TS - S#st_r.prev_flush_ts;
553 | S#st_r.flush_count >= S#st_r.flush_close_max ->
554 | close;
555 | true ->
556 | opened
557 | end,
558 | flush_samples(
559 | FileS,
560 | sampler_maybe_sample1(
561 | TS, S#st_r{prev_sample_ts=TS,prev_flush_ts=TS}),
562 | Opts);
563 | TS - S#st_r.prev_sample_ts >= S#st_r.sample_tout ->
564 | % Sample and accumulate
565 | sampler_maybe_sample1(TS, S#st_r{prev_sample_ts=TS});
566 | true ->
567 | S
568 | end.
569 |
570 | sampler_maybe_sample1(TS, #st_r{ps_grps=PsGrps}=S) ->
571 | Grp = rand:uniform(11),
572 | case PsGrps of
573 | #{Grp:=Ps0} ->
574 | case maps:fold(fun sample_p/3, {undefined,Ps0}, Ps0) of
575 | {#{}=Stks,#{}=Ps1} ->
576 | Samples = [{TS,Stks}|S#st_r.samples],
577 | S#st_r{samples=Samples,ps_grps=PsGrps#{Grp:=Ps1}};
578 | {undefined,_} ->
579 | S
580 | end;
581 | #{} ->
582 | S
583 | end.
584 |
585 | system_continue(Parent, _, {S,Opts}) ->
586 | sampler_loop(Parent, nil, S, Opts).
587 |
588 | system_get_state({S,Opts}) ->
589 | {ok,{{'$hidden',erts_debug:size(S)},Opts}}.
590 |
591 | system_terminate(Reason, _, _, {S,Opts}) ->
592 | try flush_samples(close, S, Opts)
593 | after trace_send_off()
594 | end,
595 | exit(Reason).
596 |
597 | %%% -- Helpers -----------------------------------------------------------------
598 |
599 | chk_start_opts(#{}=Opts0) ->
600 | Opts1 =
601 | lists:foldl(
602 | fun(K, Acc) ->
603 | Acc#{K=>chk_start_opt(K, maps:get(K, Acc, undefined))}
604 | end, Opts0, [basename,sample_interval,flush_interval,
605 | flush_close_max,flatten_recursive,files_max,
606 | auto_select,auto_process_opts]),
607 | #{flush_interval:=FlushMs,sample_interval:=SampleMs} = Opts1,
608 | (FlushMs - 1000) < SampleMs andalso error({badarg,flush_interval}),
609 | Opts1.
610 |
611 | chk_start_opt(basename, V) when is_list(V); is_binary(V) ->
612 | V;
613 | chk_start_opt(flatten_recursive, V) when is_boolean(V) ->
614 | V;
615 | chk_start_opt(sample_interval, V) when is_integer(V), V>0 ->
616 | V;
617 | chk_start_opt(flush_interval, V) when is_integer(V), V>0 ->
618 | V;
619 | chk_start_opt(flush_close_max, V) when is_integer(V), V>=0 ->
620 | V;
621 | chk_start_opt(flush_close_max, infinity) ->
622 | 0;
623 | chk_start_opt(files_max, infinity) ->
624 | infinity;
625 | chk_start_opt(files_max, V) when is_integer(V), V>0 ->
626 | V;
627 | chk_start_opt(auto_select, {top,0}) ->
628 | undefined;
629 | chk_start_opt(auto_select, {top,0,Ival}) when is_integer(Ival), Ival>0 ->
630 | undefined;
631 | chk_start_opt(auto_select, {top,N}) ->
632 | chk_start_opt(auto_select, {top,N,?DFLT_TOP_INTERVAL});
633 | chk_start_opt(auto_select, {top,N,Ival}=V)
634 | when is_integer(N), N>0, is_integer(Ival), Ival>0
635 | ->
636 | V;
637 | chk_start_opt(auto_process_opts, undefined) ->
638 | chk_start_opt(auto_process_opts, #{});
639 | chk_start_opt(auto_process_opts, V) ->
640 | chk_p_opts(V, self_only);
641 | % ^^^ Default for auto sampled processes as opposed to 'spawn'
642 | % which is the default for explicitly sampled processes.
643 |
644 | chk_start_opt(K, undefined) ->
645 | dflt_start_opt(K);
646 | chk_start_opt(K, V) when is_boolean(V) ->
647 | error({badarg,{K,V}}).
648 |
649 | dflt_start_opt(basename) ->
650 | {home,Home} = lists:keyfind(home, 1, init:get_arguments()),
651 | filename:join(Home, ?MODULE);
652 | dflt_start_opt(flatten_recursive) ->
653 | true;
654 | dflt_start_opt(sample_interval) ->
655 | ?DFLT_SAMPLE_INTERVAL;
656 | dflt_start_opt(flush_interval) ->
657 | ?DFLT_FLUSH_INTERVAL;
658 | dflt_start_opt(flush_close_max) ->
659 | ?DFLT_FLUSH_CLOSE_MAX;
660 | dflt_start_opt(files_max) ->
661 | ?DFLT_FILES_MAX;
662 | dflt_start_opt(auto_select) ->
663 | undefined.
664 |
665 | chk_p_opts(#{}=Opts, DfltViral) ->
666 | lists:foldl(
667 | fun
668 | (viral, Acc) ->
669 | case chk_p_opt(viral, maps:get(viral, Acc, undefined)) of
670 | undefined ->
671 | Acc#{viral=>DfltViral};
672 | V ->
673 | Acc#{viral=>V}
674 | end;
675 | (K, Acc) ->
676 | Acc#{K=>chk_p_opt(K, maps:get(K, Acc, undefined))}
677 | end, Opts, [viral,status]).
678 |
679 | chk_p_opt(viral, true) ->
680 | spawn_and_send;
681 | chk_p_opt(viral, false) ->
682 | self_only;
683 | chk_p_opt(viral, V)
684 | when V=:=spawn; V=:=spawn_and_send; V=:=send; V=:=self_only
685 | ->
686 | V;
687 | chk_p_opt(status, V)
688 | when V=:=all; V=:=exiting; V=:=garbage_collecting; V=:=waiting;
689 | V=:=running; V=:=runnable; V=:=suspended
690 | ->
691 | V;
692 | chk_p_opt(viral, undefined) ->
693 | undefined; % Intentional. See chk_p_opts/2.
694 | chk_p_opt(status, undefined) ->
695 | all;
696 | chk_p_opt(K, V) ->
697 | error({badarg,{K,V}}).
698 |
699 | viral_to_traceopts(self_only) ->
700 | [];
701 | viral_to_traceopts(spawn) ->
702 | [set_on_spawn];
703 | viral_to_traceopts(spawn_and_send) ->
704 | [send,set_on_spawn]; % NB: We rely on 'send' always being at head.
705 | viral_to_traceopts(send) ->
706 | [send]. % Ditto NB above.
707 |
708 | sample_p(P, {Filter,PrevMetrics}, {Stks,Ps}) ->
709 | case erlang:process_info(P, [status]) of
710 | [{status,Status}] when Filter=:=all; Status=:=Filter ->
711 | case erlang:process_info(
712 | P, [current_function,current_stacktrace,initial_call,
713 | dictionary,reductions,message_queue_len,
714 | memory,total_heap_size,garbage_collection_info,
715 | garbage_collection])
716 | of
717 | [{current_function,CurrMFA0},{current_stacktrace,Stk},
718 | {initial_call,Init},{dictionary,Dict}|CurrMetrics]
719 | ->
720 | % TODO: Confirm that this may be different from head of
721 | % current_stacktrace. If not, remove it.
722 | CurrMFA1 =
723 | case CurrMFA0 of
724 | undefined -> native;
725 | _ -> CurrMFA0
726 | end,
727 | Inits =
728 | case lists:keyfind('$initial_call', 1, Dict) of
729 | {'$initial_call',{M,F,N}}=GenInit
730 | when is_atom(M), is_atom(F), is_integer(N)
731 | ->
732 | [Init,GenInit];
733 | _ ->
734 | [Init]
735 | end,
736 | MetricsDelta = metrics_delta(PrevMetrics, CurrMetrics),
737 | Sample = {Status,MetricsDelta,[CurrMFA1|Stk],Inits},
738 | if
739 | undefined=:=Stks ->
740 | {#{P=>Sample},Ps#{P:={Filter,CurrMetrics}}};
741 | true ->
742 | {Stks#{P=>Sample},Ps#{P:={Filter,CurrMetrics}}}
743 | end;
744 | undefined ->
745 | {Stks,Ps} % TODO: Remove P for st_r.ps so we don't have to trace exits
746 | end;
747 | [{status,_}] ->
748 | {Stks,Ps};
749 | undefined ->
750 | {Stks,Ps} % TODO: Ditto above.
751 | end.
752 |
753 | metrics_delta(undefined, [{_,[]}=PairB|TB]) ->
754 | [PairB|metrics_delta(undefined, TB)];
755 | metrics_delta([{K,[]}|TA], [{K,[]}=PairB|TB]) ->
756 | [PairB|metrics_delta(TA, TB)];
757 | metrics_delta(undefined, [{garbage_collection,LB}|TB]) ->
758 | [{garbage_collection,[{K,V,0} || {K,V} <- LB, K=:=minor_gcs]}
759 | |metrics_delta(undefined, TB)];
760 | metrics_delta([{garbage_collection,LA}|TA],
761 | [{garbage_collection,LB}|TB])
762 | ->
763 | {_,VA} = lists:keyfind(minor_gcs, 1, LA),
764 | {_,VB} = lists:keyfind(minor_gcs, 1, LB),
765 | VDelta = if VB>VA -> VB-VA; true -> VB end,
766 | % NB: VB-VA ^^^ doesn't make sense if there's been one or more full sweeps
767 | % between samples [1]. If VBVB, we need to somehow find detect if there's been a full
771 | % sweep and similarly take VB as the delta. This case we're currently
772 | % not handling.
773 | % [1] https://github.com/erlang/otp/blob/de00be8f6723dededcbcff6f635394167b609367/erts/emulator/beam/erl_bif_info.c#L1812
774 | % https://github.com/erlang/otp/blob/a67f636bcf14dd5eebea9b1bf1e7b89b38895b22/erts/emulator/beam/erl_gc.c#L1855
775 | [{garbage_collection,[{minor_gcs,VB,VDelta}]}|metrics_delta(TA, TB)];
776 | metrics_delta(undefined, [{garbage_collection_info,LB}|TB]) ->
777 | [{garbage_collection_info,[{K,VB,0} || {K,VB} <- LB]}
778 | |metrics_delta(undefined, TB)];
779 | metrics_delta([{garbage_collection_info,LA}|TA],
780 | [{garbage_collection_info,LB}|TB])
781 | ->
782 | [{garbage_collection_info,
783 | fun
784 | Delta([{K,VA}|TAInner], [{K,VB}|TBInner]) ->
785 | [{K,VB,VB-VA}|Delta(TAInner, TBInner)];
786 | Delta([], []) ->
787 | []
788 | end(LA, LB)} | metrics_delta(TA, TB)];
789 | metrics_delta(undefined, [{reductions,VB}|TB]) ->
790 | [{reductions,VB,1}|metrics_delta(undefined, TB)];
791 | metrics_delta(undefined, [{K,VB}|TB]) ->
792 | [{K,VB,0}|metrics_delta(undefined, TB)];
793 | metrics_delta([{reductions,VA}|TA], [{reductions,VB}|TB]) ->
794 | [{reductions,VB,case VB-VA of X when X>=0 -> X+1; X -> -X end}
795 | % XXX: Negative does happen ^
796 | % (maybe due to a scheduler race?)
797 | |metrics_delta(TA, TB)];
798 | metrics_delta([{K,VA}|TA], [{K,VB}|TB]) ->
799 | [{K,VB,VB-VA}|metrics_delta(TA, TB)];
800 | metrics_delta(undefined, []) ->
801 | [];
802 | metrics_delta([], []) ->
803 | [].
804 |
805 | flush_samples(Dur, #st_r{file=undefined,samples=[]}=S, #{}) when is_integer(Dur)
806 | ->
807 | S;
808 | flush_samples(Dur, #st_r{file=undefined}=S, #{basename:=BseNme,
809 | files_max:=Max}=Opts)
810 | when is_integer(Dur)
811 | ->
812 | StartSysTm = erlang:system_time(perf_counter) - Dur,
813 | {{Yy,Mn,Dd},{Hh,Mm,Ss}} =
814 | calendar:system_time_to_local_time(StartSysTm, perf_counter),
815 | Leaf = io_lib:format(
816 | "~4..0w-~2..0w-~2..0w@~2..0w~2..0w.~2..0w.erl_perf.log",
817 | [Yy,Mn,Dd,Hh,Mm,Ss]),
818 | Path = filename:join(BseNme, Leaf),
819 | _ = filelib:ensure_dir(Path),
820 | {ok,Fd} = file:open(Path, [append,read,
821 | {delayed_write,?DELAY_SIZE,?DELAY_INTERVAL}]),
822 | flush_samples1(Fd, S#st_r.samples, S#st_r.reg_names, Opts),
823 | file_flush_delayed(Fd),
824 | Paths =
825 | case S#st_r.paths of
826 | undefined when Max=:=undefined ->
827 | undefined;
828 | {N,Q0} when N=:=Max ->
829 | {{value,FPath},Q1} = queue:out(Q0),
830 | _ = file:delete(FPath),
831 | {Max,queue:in(Path, Q1)};
832 | {N,Q} when N>=0, N
833 | {N+1,queue:in(Path, Q)}
834 | end,
835 | S#st_r{samples=[],flush_count=0,paths=Paths,file=Fd};
836 | flush_samples(close, #st_r{file=undefined,samples=[]}=S, #{}) ->
837 | S;
838 | flush_samples(close, #st_r{file=undefined,samples=[_|_]}=S, #{}=Opts) ->
839 | flush_samples(close, flush_samples(0, S, Opts), Opts);
840 | flush_samples(close, #st_r{}=S, Opts) ->
841 | Fd = flush_samples1(S#st_r.file, S#st_r.samples, S#st_r.reg_names, Opts),
842 | file_flush_delayed(Fd),
843 | _ = file:close(Fd),
844 | S#st_r{samples=[],flush_count=0,file=undefined};
845 | flush_samples(opened, #st_r{samples=[]}=S, #{}) ->
846 | S;
847 | flush_samples(opened, #st_r{flush_count=FlushCount}=S, #{}=Opts) ->
848 | S#st_r{samples=[],flush_count=FlushCount+1,
849 | file=flush_samples1(
850 | S#st_r.file, S#st_r.samples, S#st_r.reg_names, Opts)}.
851 |
852 | flush_samples1(Fd, [], #{}, #{}) ->
853 | Fd;
854 | flush_samples1(Fd, [_|_]=Samples, Nmes, #{flatten_recursive:=Flatten}) ->
855 | flush_samples2(Fd, lists:reverse(Samples), Nmes, Flatten).
856 | % TODO: ^^^ Does the order really matter to FlameScope or
857 | % flamegraph.pl? If not, no need to bother with
858 | % reverse. Mabye a start op to retain order?
859 |
860 | flush_samples2(Fd, [], _, _) ->
861 | Fd;
862 | flush_samples2(Fd, [{TS,#{}=Ps}|TSamples], Nmes, Flatten) ->
863 | % For format, see...
864 | % https://github.com/Netflix/flamescope/blob/141238b4f69feee1d026c2156951eb78857e3953/app/perf/regexp.py#L24
865 | % https://github.com/Netflix/flamescope/blob/141238b4f69feee1d026c2156951eb78857e3953/app/perf/flame_graph.py#L185
866 | % https://github.com/Netflix/flamescope/blob/141238b4f69feee1d026c2156951eb78857e3953/app/common/fileutil.py#L45
867 | maps:fold(
868 | fun
869 | Fold(P, {Status,Metrics,Stk,[_|_]=Inits}, sample) ->
870 | PStr = pid_str(P),
871 | %Nme = case maps:get(P, Nmes, 0) of
872 | % 0 -> PStr;
873 | % X -> atom_to_list(X)
874 | % end,
875 | Us = pc_to_microsecs(TS),
876 | io:put_chars(
877 | Fd, [atom_to_list(Status),$\t,PStr," [000] ",
878 | microsecs_to_secs_str(Us),": cpu-clock:"]),
879 | flush_metrics(Fd, Metrics),
880 | io:put_chars(Fd, "\n"),
881 | Fold(P, Stk, Inits);
882 | Fold(P, [{M,F,N,_}]=Stk, [{M,F,N}|TNext]) ->
883 | Fold(P, Stk, TNext);
884 | Fold(P, [{M,F,N}]=Stk, [{M,F,N}|TNext]) ->
885 | Fold(P, Stk, TNext);
886 | Fold(P, [{M,F,N,_}]=Stk, [{'$initial_call',{M,F,N}}]) ->
887 | Fold(P, Stk, []);
888 | Fold(P, [{M,F,N}]=Stk, [{'$initial_call',{M,F,N}}]) ->
889 | Fold(P, Stk, []);
890 | Fold(P, [{M,F,N},{M,F,N,_}=Second|TStk], Next) ->
891 | Fold(P, [Second|TStk], Next);
892 | Fold(P, [{M,F,N}|TStk], Next) ->
893 | Fold(P, [{M,F,N,[]}|TStk], Next);
894 | Fold(P, [{M,F,N,_},{M,F,N,_}=Recursive|TStk], Next)
895 | when Flatten
896 | ->
897 | Fold(P, [Recursive|TStk], Next);
898 | Fold(P, [{M,F,N,[{file,File},{line,Ln}|_]}|TStk], Next) ->
899 | io:put_chars(Fd, ["\tffffffff ",atom_to_list(M),$:,
900 | atom_to_list(F),$/,integer_to_list(N),$;,
901 | integer_to_list(Ln)," (",File,")\n"]),
902 | Fold(P, TStk, Next);
903 | Fold(P, [{M,F,N,[]}|TStk], Next) ->
904 | MStr = atom_to_list(M),
905 | io:put_chars(Fd, ["\tffffffff ",MStr,$:,atom_to_list(F),$/,
906 | integer_to_list(N)," (",MStr,".erl)\n"]),
907 | Fold(P, TStk, Next);
908 | Fold(P, [{'$initial_call',{M,F,N}}|TStk], Next) ->
909 | MStr = atom_to_list(M),
910 | io:put_chars(Fd, ["\tffffffff (",MStr,$:,atom_to_list(F),$/,
911 | integer_to_list(N),") (",MStr,".erl)\n"]),
912 | Fold(P, TStk, Next);
913 | Fold(P, [native|TStk], Next) ->
914 | io:put_chars(Fd, ["\tffffffff native ()\n"]),
915 | Fold(P, TStk, Next);
916 | Fold(P, [], [_|_]=Inits) ->
917 | Fold(P, Inits, sample);
918 | Fold(P, [], []) ->
919 | Fold(P, [], sample);
920 | Fold(_, [], sample) ->
921 | io:put_chars(Fd, "\n"),
922 | sample
923 | end, sample, Ps),
924 | flush_samples2(Fd, TSamples, Nmes, Flatten).
925 |
926 | flush_metrics(Fd, [{_,[]}|T]) ->
927 | flush_metrics(Fd, T);
928 | flush_metrics(Fd, [{_,[_|_]=L}|T]) ->
929 | io:put_chars(
930 | Fd, [["\tmetric_erlang_",atom_to_list(K),": ",integer_to_list(V),", ",
931 | integer_to_list(Delta)] || {K,V,Delta} <- L]),
932 | flush_metrics(Fd, T);
933 | flush_metrics(Fd, [{K,V,Delta}|T]) ->
934 | io:put_chars(
935 | Fd, ["\tmetric_erlang_",atom_to_list(K),": ",integer_to_list(V),", ",
936 | integer_to_list(Delta)]),
937 | flush_metrics(Fd, T);
938 | flush_metrics(_, []) ->
939 | ok.
940 |
941 | %%%
942 | %% Delete all but the lexicographically last `files_max' *.erl_perf.log files in
943 | %% the basename dir, returning their count and paths.
944 | %%
945 | del_excess_files(#{files_max:=infinity}) ->
946 | undefined;
947 | del_excess_files(#{files_max:=Max,basename:=BseNme}) when Max>=0 ->
948 | Paths = filelib:wildcard(filename:join(BseNme,"*.erl_perf.log")),
949 | lists:foldr(
950 | fun
951 | (Path, {N,_}=Acc) when N=:=Max ->
952 | _ = file:delete(Path),
953 | Acc;
954 | (Path, {N,Q}=Acc) when N
955 | case file_del_if_zero(Path) of
956 | true ->
957 | _ = file:delete(Path),
958 | Acc;
959 | false ->
960 | {N+1,queue:in_r(Path, Q)}
961 | end
962 | end, {0,queue:new()}, Paths).
963 |
964 | file_del_if_zero(Path) ->
965 | case file:read_file_info(Path) of
966 | {ok,#file_info{size=0}} ->
967 | _ = file:delete(Path),
968 | true;
969 | {ok,#file_info{}} ->
970 | false;
971 | {error,_} ->
972 | false
973 | end.
974 |
975 | file_flush_delayed(Fd) ->
976 | case file:read(Fd, 1) of
977 | {ok,_} -> ok;
978 | eof -> ok;
979 | {error,enoent} -> ok;
980 | {error,enotdir} -> ok
981 | end.
982 |
983 | %%%
984 | %% Flamescope and flamegraph.pl don't like the "<" and ">" in the process name
985 | %% and PID columns of the output file, so strip them from the erlang process
986 | %% pid.
987 | %%
988 | pid_str(P) when is_pid(P) ->
989 | pid_str(pid_to_list(P));
990 | pid_str([$>]) ->
991 | [];
992 | pid_str([$<|T]) ->
993 | pid_str(T);
994 | pid_str([C|T]) ->
995 | [C|pid_str(T)].
996 |
997 | maybe_trace_send_on([send|_]) ->
998 | erlang:trace_pattern(send, true, []);
999 | maybe_trace_send_on([_|_]) ->
1000 | 0;
1001 | maybe_trace_send_on([]) ->
1002 | 0.
1003 |
1004 | trace_send_off() ->
1005 | erlang:trace_pattern(send, false, []).
1006 |
1007 | trace_p_on(P, Sampler, [_|_]=ViralTraceOpts) ->
1008 | TraceOpts = [{tracer,Sampler},procs|ViralTraceOpts],
1009 | if
1010 | P=:=self() ->
1011 | erlang:trace(P, true, TraceOpts);
1012 | true ->
1013 | try erlang:trace(P, true, TraceOpts)
1014 | catch error:badarg ->
1015 | case is_process_alive(P) of
1016 | false -> 0;
1017 | true -> error(badarg)
1018 | end
1019 | end
1020 | end.
1021 |
1022 | trace_p_off(P) when P=:=self() ->
1023 | erlang:trace(P, false, [all]);
1024 | trace_p_off(P) ->
1025 | try erlang:trace(P, false, [all])
1026 | catch error:badarg ->
1027 | case is_process_alive(P) of
1028 | false -> 0;
1029 | true -> error(badarg)
1030 | end
1031 | end.
1032 |
1033 | pc_from_millisecs(Ms) ->
1034 | erlang:convert_time_unit(Ms, millisecond, perf_counter).
1035 |
1036 | pc_to_microsecs(Pc) ->
1037 | erlang:convert_time_unit(Pc, perf_counter, microsecond).
1038 |
1039 | microsecs_to_secs_str(Us) ->
1040 | SubSecs = Us rem 1000000,
1041 | [integer_to_list(Us div 1000000),$.|
1042 | if
1043 | SubSecs>99999 -> integer_to_list(SubSecs);
1044 | SubSecs>9999 -> [$0|integer_to_list(SubSecs)];
1045 | SubSecs>999 -> [$0,$0|integer_to_list(SubSecs)];
1046 | SubSecs>99 -> [$0,$0,$0|integer_to_list(SubSecs)];
1047 | SubSecs>9 -> [$0,$0,$0,$0|integer_to_list(SubSecs)];
1048 | SubSecs>0 -> [$0,$0,$0,$0,$0|integer_to_list(SubSecs)];
1049 | SubSecs=:=0 -> "000000"
1050 | end].
1051 |
1052 | is_terminate_crash(normal) -> false;
1053 | is_terminate_crash(shutdown) -> false;
1054 | is_terminate_crash({shutdown,_}) -> false;
1055 | is_terminate_crash(killed) -> false;
1056 | is_terminate_crash(_) -> true.
1057 |
--------------------------------------------------------------------------------
/src/seltor@flame_prof.erl:
--------------------------------------------------------------------------------
1 | %%
2 | %% Copyright 2020 Medical-Objects Pty Ltd.
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%
16 |
17 | %%% ============================================================================
18 | %%% @doc Process selector for `flame_prof'
19 | %%%
20 | %%% This process auto selects processes to be profiled by `flame_prof' on a
21 | %%% timer and sends them to `flame_prof'.
22 | %%% @end
23 | %%% ============================================================================
24 | -module(seltor@flame_prof).
25 |
26 | %%% -- Public Control --
27 | -export([start_link/2]).
28 |
29 | %%% -- Private Interface --
30 | -behaviour(gen_server).
31 | -export([init/1, handle_continue/2,
32 | handle_call/3, handle_cast/2, handle_info/2,
33 | terminate/2, code_change/3, format_status/2]).
34 |
35 | -record(st_r,
36 | {sampler :: pid(),
37 | auto_select :: {top,ProcessCount :: pos_integer(),
38 | IntervalMillisecs :: pos_integer(),
39 | IntervalPc :: integer()},
40 | snapshot=#{} :: #{pid() => {ReductionsDelta :: integer(),
41 | Reductions :: non_neg_integer()}}}).
42 |
43 | %%% -- Public Control ----------------------------------------------------------
44 |
45 | start_link({top,_,_}=AutoSel, SysOpts) when is_list(SysOpts) ->
46 | gen_server:start_link({local,?MODULE}, ?MODULE, {AutoSel,self()}, SysOpts).
47 |
48 | %%% -- Public Interface --------------------------------------------------------
49 |
50 | %%% -- 'gen_server' callbacks ---
51 |
52 | init({AutoSel,Sampler}) ->
53 | process_flag(trap_exit, true),
54 | S = #st_r{sampler=Sampler,auto_select=AutoSel},
55 | {ok,S,{continue,init}}.
56 |
57 | handle_continue(init, #st_r{auto_select={top,PCount,Ival}}=S) ->
58 | {Ps,Snap} = auto_select_top(PCount, get_reductions_new()),
59 | []=/=Ps andalso (S#st_r.sampler ! {auto_selected,Ps}),
60 | {noreply,S#st_r{snapshot=Snap},Ival}.
61 |
62 | handle_call(never_match, _, S) ->
63 | {stop,never_match,S}.
64 |
65 | handle_cast(never_match, S) ->
66 | {stop,never_match,S}.
67 |
68 | handle_info(timeout, #st_r{auto_select={top,PCount,Ival}}=S) ->
69 | {Ps,Snap} = auto_select_top(PCount, get_reductions_delta(S#st_r.snapshot)),
70 | []=/=Ps andalso (S#st_r.sampler ! {auto_selected,Ps}),
71 | {noreply,S#st_r{snapshot=Snap},Ival};
72 | handle_info({'EXIT',Sampler,Reason}, #st_r{sampler=Sampler}=S) ->
73 | {stop,Reason,S};
74 | handle_info({'EXIT',SamplerParent,Reason}, #st_r{}=S) ->
75 | [_,SamplerParent] = get('$ancestors'),
76 | {stop,Reason,S}.
77 |
78 | terminate(_, _) ->
79 | ok.
80 |
81 | code_change(_, #st_r{auto_select={top,_,_},snapshot=#{}}=S, _) ->
82 | {ok,S}.
83 |
84 | format_status(_, [_,#st_r{snapshot=Snap}=S]) ->
85 | [{data, [{"State",S#st_r{snapshot={'$hidden',erts_debug:size(Snap)}}}]}].
86 |
87 | %%% -- Helpers -----------------------------------------------------------------
88 |
89 | %%%
90 | %% @doc Get the top N processes by reductions.
91 | %%
92 | auto_select_top(N, {RedPairs,Snap}) when is_integer(N), N>0 ->
93 | % See {@link observer_backend:etop_collect/1} [https://github.com/erlang/otp/blob/a67f636bcf14dd5eebea9b1bf1e7b89b38895b22/lib/runtime_tools/src/observer_backend.erl#L325]
94 | % See {@link observer_backend:etop_collect/2} [https://github.com/erlang/otp/blob/a67f636bcf14dd5eebea9b1bf1e7b89b38895b22/lib/runtime_tools/src/observer_backend.erl#L371]
95 | Decc = lists:sort( % <<< TODO: Use more efficient partial sort instead.
96 | fun({_,{_,_}=PairA}, {_,{_,_}=PairB}) -> PairA>PairB end,
97 | RedPairs),
98 | {auto_select_top1(N, Decc), Snap}.
99 |
100 | auto_select_top1(0, Decc) when is_list(Decc) ->
101 | [];
102 | auto_select_top1(N, [{P,{_,_}}|T]) ->
103 | [P|auto_select_top1(N-1, T)];
104 | auto_select_top1(_, []) ->
105 | [].
106 |
107 | get_reductions_new() ->
108 | do_get_reductions(processes(), #{}, #{}, [], true).
109 |
110 | get_reductions_delta(PrevSnap) ->
111 | do_get_reductions(processes(), PrevSnap, #{}, [], true).
112 |
113 | do_get_reductions([H|T], PrevSnap, CurrSnap, Acc, WantDelta) ->
114 | case process_info(H, reductions) of
115 | {_,CurrReds} when WantDelta ->
116 | CurrPair =
117 | case PrevSnap of
118 | #{H:=PrevPair} ->
119 | {_,PrevReds} = PrevPair,
120 | {CurrReds-PrevReds,CurrReds};
121 | #{} ->
122 | {0,CurrReds}
123 | end,
124 | do_get_reductions(
125 | T, maps:remove(H, PrevSnap), CurrSnap#{H=>CurrPair},
126 | [{H,CurrPair}|Acc], true);
127 | {_,CurrReds} ->
128 | CurrPair = {0,CurrReds},
129 | do_get_reductions(
130 | T, maps:remove(H, PrevSnap), CurrSnap#{H=>CurrPair},
131 | [{H,CurrPair}|Acc], false);
132 | undefined ->
133 | do_get_reductions(
134 | T, maps:remove(H, PrevSnap), CurrSnap, Acc, WantDelta)
135 | end;
136 | do_get_reductions([], #{}, #{}=CurrSnap, Acc, _) ->
137 | {Acc,CurrSnap}.
138 |
139 |
--------------------------------------------------------------------------------