-
Notifications
You must be signed in to change notification settings - Fork 277
/
Copy pathrecon_alloc.erl
780 lines (733 loc) · 32.2 KB
/
recon_alloc.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
%%% @author Fred Hebert <[email protected]>
%%% [http://ferd.ca/]
%%% @author Lukas Larsson <[email protected]>
%%% @doc Functions to deal with
%%% <a href="http://www.erlang.org/doc/man/erts_alloc.html">Erlang's memory
%%% allocators</a>, or particularly, to try to present the allocator data
%%% in a way that makes it simpler to discover possible problems.
%%%
%%% Tweaking Erlang memory allocators and their behaviour is a very tricky
%%% ordeal whenever you have to give up the default settings. This module
%%% (and its documentation) will try and provide helpful pointers to help
%%% in this task.
%%%
%%% This module should mostly be helpful to figure out <em>if</em> there is
%%% a problem, but will offer little help to figure out <em>what</em> is wrong.
%%%
%%% To figure this out, you need to dig deeper into the allocator data
%%% (obtainable with {@link allocators/1}), and/or have some precise knowledge
%%% about the type of load and work done by the VM to be able to assess what
%%% each reaction to individual tweak should be.
%%%
%%% A lot of trial and error might be required to figure out if tweaks have
%%% helped or not, ultimately.
%%%
%%% In order to help do offline debugging of memory allocator problems
%%% recon_alloc also has a few functions that store snapshots of the
%%% memory statistics.
%%% These snapshots can be used to freeze the current allocation values so that
%%% they do not change during analysis while using the regular functionality of
%%% this module, so that the allocator values can be saved, or that
%%% they can be shared, dumped, and reloaded for further analysis using files.
%%% See {@link snapshot_load/1} for a simple use-case.
%%%
%%% Glossary:
%%% <dl>
%%% <dt>sys_alloc</dt>
%%% <dd>System allocator, usually just malloc</dd>
%%%
%%% <dt>mseg_alloc</dt>
%%% <dd>Used by other allocators, can do mmap. Caches allocations</dd>
%%%
%%% <dt>temp_alloc</dt>
%%% <dd>Used for temporary allocations</dd>
%%%
%%% <dt>eheap_alloc</dt>
%%% <dd>Heap data (i.e. process heaps) allocator</dd>
%%%
%%% <dt>binary_alloc</dt>
%%% <dd>Global binary heap allocator</dd>
%%%
%%% <dt>ets_alloc</dt>
%%% <dd>ETS data allocator</dd>
%%%
%%% <dt>driver_alloc</dt>
%%% <dd>Driver data allocator</dd>
%%%
%%% <dt>sl_alloc</dt>
%%% <dd>Short-lived memory blocks allocator</dd>
%%%
%%% <dt>ll_alloc</dt>
%%% <dd>Long-lived data (i.e. Erlang code itself) allocator</dd>
%%%
%%% <dt>fix_alloc</dt>
%%% <dd>Frequently used fixed-size data allocator</dd>
%%%
%%% <dt>std_alloc</dt>
%%% <dd>Allocator for other memory blocks</dd>
%%%
%%% <dt>carrier</dt>
%%% <dd>When a given area of memory is allocated by the OS to the
%%% VM (through sys_alloc or mseg_alloc), it is put into a 'carrier'. There
%%% are two kinds of carriers: multiblock and single block. The default
%%% carriers data is sent to are multiblock carriers, owned by a specific
%%% allocator (ets_alloc, binary_alloc, etc.). The specific allocator can
%%% thus do allocation for specific Erlang requirements within bits of
%%% memory that has been preallocated before. This allows more reuse,
%%% and we can even measure the cache hit rates {@link cache_hit_rates/0}.
%%%
%%% There is however a threshold above which an item in memory won't fit
%%% a multiblock carrier. When that happens, the specific allocator does
%%% a special allocation to a single block carrier. This is done by the
%%% allocator basically asking for space directly from sys_alloc or
%%% mseg_alloc rather than a previously multiblock area already obtained
%%% before.
%%%
%%% This leads to various allocation strategies where you decide to
%%% choose:
%%% <ol>
%%% <li>which multiblock carrier you're going to (if at all)</li>
%%% <li>which block in that carrier you're going to</li>
%%% </ol>
%%%
%%% See <a href="http://www.erlang.org/doc/man/erts_alloc.html">the official
%%% documentation on erts_alloc</a> for more details.
%%% </dd>
%%%
%%% <dt>mbcs</dt>
%%% <dd>Multiblock carriers.</dd>
%%%
%%% <dt>sbcs</dt>
%%% <dd>Single block carriers.</dd>
%%%
%%% <dt>lmbcs</dt>
%%% <dd>Largest multiblock carrier size</dd>
%%%
%%% <dt>smbcs</dt>
%%% <dd>Smallest multiblock carrier size</dd>
%%%
%%% <dt>sbct</dt>
%%% <dd>Single block carrier threshold</dd>
%%% </dl>
%%%
%%% By default all sizes returned by this module are in bytes. You can change
%%% this by calling {@link set_unit/1}.
%%%
-module(recon_alloc).
-define(UTIL_ALLOCATORS, [temp_alloc,
eheap_alloc,
binary_alloc,
ets_alloc,
driver_alloc,
sl_alloc,
ll_alloc,
fix_alloc,
std_alloc
]).
-type allocator() :: temp_alloc | eheap_alloc | binary_alloc | ets_alloc
| driver_alloc | sl_alloc | ll_alloc | fix_alloc
| std_alloc.
-type instance() :: non_neg_integer().
-type allocdata(T) :: {{allocator(), instance()}, T}.
-type allocdata_types(T) :: {{allocator(), [instance()]}, T}.
-export_type([allocator/0, instance/0, allocdata/1]).
-define(CURRENT_POS, 2). % pos in sizes tuples for current value
-define(MAX_POS, 4). % pos in sizes tuples for max value
-export([memory/1, memory/2, fragmentation/1, cache_hit_rates/0,
average_block_sizes/1, sbcs_to_mbcs/1, allocators/0,
allocators/1]).
%% Snapshot handling
-type memory() :: [{atom(),atom()}].
-type snapshot() :: {memory(),[allocdata(term())]}.
-export_type([memory/0, snapshot/0]).
-export([snapshot/0, snapshot_clear/0,
snapshot_print/0, snapshot_get/0,
snapshot_save/1, snapshot_load/1]).
%% Unit handling
-export([set_unit/1]).
%%%%%%%%%%%%%%
%%% Public %%%
%%%%%%%%%%%%%%
%% @doc Equivalent to `memory(Key, current)'.
-spec memory(used | allocated | unused) -> pos_integer()
; (usage) -> number()
; (allocated_types | allocated_instances) ->
[{allocator(), pos_integer()}].
memory(Key) -> memory(Key, current).
%% @doc reports one of multiple possible memory values for the entire
%% node depending on what is to be reported:
%%
%% <ul>
%% <li>`used' reports the memory that is actively used for allocated
%% Erlang data;</li>
%% <li>`allocated' reports the memory that is reserved by the VM. It
%% includes the memory used, but also the memory yet-to-be-used but still
%% given by the OS. This is the amount you want if you're dealing with
%% ulimit and OS-reported values. </li>
%% <li>`allocated_types' report the memory that is reserved by the
%% VM grouped into the different util allocators.</li>
%% <li>`allocated_instances' report the memory that is reserved
%% by the VM grouped into the different schedulers. Note that
%% instance id 0 is the global allocator used to allocate data from
%% non-managed threads, i.e. async and driver threads.</li>
%% <li>`unused' reports the amount of memory reserved by the VM that
%% is not being allocated.
%% Equivalent to `allocated - used'.</li>
%% <li>`usage' returns a percentage (0.0 .. 1.0) of `used/allocated'
%% memory ratios.</li>
%% </ul>
%%
%% The memory reported by `allocated' should roughly
%% match what the OS reports. If this amount is different by a large margin,
%% it may be the sign that someone is allocating memory in C directly, outside
%% of Erlang's own allocator -- a big warning sign. There are currently
%% three sources of memory alloction that are not counted towards this value:
%% The cached segments in the mseg allocator, any memory allocated as a
%% super carrier, and small pieces of memory allocated during startup
%% before the memory allocators are initialized.
%%
%% Also note that low memory usages can be the sign of fragmentation in
%% memory, in which case exploring which specific allocator is at fault
%% is recommended (see {@link fragmentation/1})
-spec memory(used | allocated | unused, current | max) -> pos_integer()
; (usage, current | max) -> number()
; (allocated_types|allocated_instances, current | max) ->
[{allocator(),pos_integer()}].
memory(used,Keyword) ->
lists:sum(lists:map(fun({_,Prop}) ->
container_size(Prop,Keyword,blocks_size)
end,util_alloc()));
memory(allocated,Keyword) ->
lists:sum(lists:map(fun({_,Prop}) ->
container_size(Prop,Keyword,carriers_size)
end,util_alloc()));
memory(allocated_types,Keyword) ->
lists:foldl(fun({{Alloc,_N},Props},Acc) ->
CZ = container_size(Props,Keyword,carriers_size),
orddict:update_counter(Alloc,CZ,Acc)
end,orddict:new(),util_alloc());
memory(allocated_instances,Keyword) ->
lists:foldl(fun({{_Alloc,N},Props},Acc) ->
CZ = container_size(Props,Keyword,carriers_size),
orddict:update_counter(N,CZ,Acc)
end,orddict:new(),util_alloc());
memory(unused,Keyword) ->
memory(allocated,Keyword) - memory(used,Keyword);
memory(usage,Keyword) ->
memory(used,Keyword) / memory(allocated,Keyword).
%% @doc Compares the block sizes to the carrier sizes, both for
%% single block (`sbcs') and multiblock (`mbcs') carriers.
%%
%% The returned results are sorted by a weight system that is
%% somewhat likely to return the most fragmented allocators first,
%% based on their percentage of use and the total size of the carriers,
%% for both `sbcs' and `mbcs'.
%%
%% The values can both be returned for `current' allocator values, and
%% for `max' allocator values. The current values hold the present allocation
%% numbers, and max values, the values at the peak. Comparing both together
%% can give an idea of whether the node is currently being at its memory peak
%% when possibly leaky, or if it isn't. This information can in turn
%% influence the tuning of allocators to better fit sizes of blocks and/or
%% carriers.
-spec fragmentation(current | max) -> [allocdata([{atom(), term()}])].
fragmentation(Keyword) ->
WeighedData = [begin
BlockSbcs = container_value(Props, Keyword, sbcs, blocks_size),
CarSbcs = container_value(Props, Keyword, sbcs, carriers_size),
BlockMbcs = container_value(Props, Keyword, mbcs, blocks_size),
CarMbcs = container_value(Props, Keyword, mbcs, carriers_size),
{Weight, Vals} = weighed_values({BlockSbcs,CarSbcs},
{BlockMbcs,CarMbcs}),
{Weight, {Allocator,N}, Vals}
end || {{Allocator, N}, Props} <- util_alloc()],
[{Key,Val} || {_W, Key, Val} <- lists:reverse(lists:sort(WeighedData))].
%% @doc looks at the `mseg_alloc' allocator (allocator used by all the
%% allocators in {@link allocator()}) and returns information relative to
%% the cache hit rates. Unless memory has expected spiky behaviour, it should
%% usually be above 0.80 (80%).
%%
%% Cache can be tweaked using three VM flags: `+MMmcs', `+MMrmcbf', and
%% `+MMamcbf'.
%%
%% `+MMmcs' stands for the maximum amount of cached memory segments. Its
%% default value is '10' and can be anything from 0 to 30. Increasing
%% it first and verifying if cache hits get better should be the first
%% step taken.
%%
%% The two other options specify what are the maximal values of a segment
%% to cache, in relative (in percent) and absolute terms (in kilobytes),
%% respectively. Increasing these may allow more segments to be cached, but
%% should also add overheads to memory allocation. An Erlang node that has
%% limited memory and increases these values may make things worse on
%% that point.
%%
%% The values returned by this function are sorted by a weight combining
%% the lower cache hit joined to the largest memory values allocated.
-spec cache_hit_rates() -> [{{instance,instance()}, [{Key,Val}]}] when
Key :: hit_rate | hits | calls,
Val :: term().
cache_hit_rates() ->
WeighedData = [begin
Mem = proplists:get_value(memkind, Props),
{_,Hits} = lists:keyfind(cache_hits, 1, proplists:get_value(status,Mem)),
{_,Giga,Ones} = lists:keyfind(mseg_alloc,1,proplists:get_value(calls,Mem)),
Calls = 1000000000*Giga + Ones,
HitRate = usage(Hits,Calls),
Weight = (1.00 - HitRate)*Calls,
{Weight, {instance,N}, [{hit_rate,HitRate}, {hits,Hits}, {calls,Calls}]}
end || {{_, N}, Props} <- alloc([mseg_alloc])],
[{Key,Val} || {_W,Key,Val} <- lists:reverse(lists:sort(WeighedData))].
%% @doc Checks all allocators in {@link allocator()} and returns the average
%% block sizes being used for `mbcs' and `sbcs'. This value is interesting
%% to use because it will tell us how large most blocks are.
%% This can be related to the VM's largest multiblock carrier size
%% (`lmbcs') and smallest multiblock carrier size (`smbcs') to specify
%% allocation strategies regarding the carrier sizes to be used.
%%
%% This function isn't exceptionally useful unless you know you have some
%% specific problem, say with sbcs/mbcs ratios (see {@link sbcs_to_mbcs/1})
%% or fragmentation for a specific allocator, and want to figure out what
%% values to pick to increase or decrease sizes compared to the currently
%% configured value.
%%
%% Do note that values for `lmbcs' and `smbcs' are going to be rounded up
%% to the next power of two when configuring them.
-spec average_block_sizes(current | max) -> [{allocator(), [{Key,Val}]}] when
Key :: mbcs | sbcs,
Val :: number().
average_block_sizes(Keyword) ->
Dict = lists:foldl(fun({{Instance,_},Props},Dict0) ->
CarSbcs = container_value(Props, Keyword, sbcs, blocks),
SizeSbcs = container_value(Props, Keyword, sbcs, blocks_size),
CarMbcs = container_value(Props, Keyword, mbcs, blocks),
SizeMbcs = container_value(Props, Keyword, mbcs, blocks_size),
Dict1 = dict:update_counter({Instance,sbcs,count},CarSbcs,Dict0),
Dict2 = dict:update_counter({Instance,sbcs,size},SizeSbcs,Dict1),
Dict3 = dict:update_counter({Instance,mbcs,count},CarMbcs,Dict2),
Dict4 = dict:update_counter({Instance,mbcs,size},SizeMbcs,Dict3),
Dict4
end,
dict:new(),
util_alloc()),
average_group(average_calc(lists:sort(dict:to_list(Dict)))).
%% @doc compares the amount of single block carriers (`sbcs') vs the
%% number of multiblock carriers (`mbcs') for each individual allocator in
%% {@link allocator()}.
%%
%% When a specific piece of data is allocated, it is compared to a threshold,
%% called the 'single block carrier threshold' (`sbct'). When the data is
%% larger than the `sbct', it gets sent to a single block carrier. When the
%% data is smaller than the `sbct', it gets placed into a multiblock carrier.
%%
%% mbcs are to be preferred to sbcs because they basically represent pre-
%% allocated memory, whereas sbcs will map to one call to sys_alloc
%% or mseg_alloc, which is more expensive than redistributing
%% data that was obtained for multiblock carriers. Moreover, the VM is able to
%% do specific work with mbcs that should help reduce fragmentation in ways
%% sys_alloc or mmap usually won't.
%%
%% Ideally, most of the data should fit inside multiblock carriers. If
%% most of the data ends up in `sbcs', you may need to adjust the multiblock
%% carrier sizes, specifically the maximal value (`lmbcs') and the threshold
%% (`sbct'). On 32 bit VMs, `sbct' is limited to 8MBs, but 64 bit VMs can go
%% to pretty much any practical size.
%%
%% Given the value returned is a ratio of sbcs/mbcs, the higher the value,
%% the worst the condition. The list is sorted accordingly.
-spec sbcs_to_mbcs(max | current) -> [allocdata(term())].
sbcs_to_mbcs(Keyword) ->
WeightedList = [begin
Sbcs = container_value(Props, Keyword, sbcs, blocks),
Mbcs = container_value(Props, Keyword, mbcs, blocks),
Ratio = case {Sbcs, Mbcs} of
{0,0} -> 0;
{_,0} -> infinity; % that is bad!
{_,_} -> Sbcs / Mbcs
end,
{Ratio, {Allocator,N}}
end || {{Allocator, N}, Props} <- util_alloc()],
[{Alloc,Ratio} || {Ratio,Alloc} <- lists:reverse(lists:sort(WeightedList))].
%% @doc returns a dump of all allocator settings and values
-spec allocators() -> [allocdata(term())].
allocators() ->
UtilAllocators = erlang:system_info(alloc_util_allocators),
Allocators = [sys_alloc,mseg_alloc|UtilAllocators],
[{{A,N}, format_alloc(A, Props)} ||
A <- Allocators,
Allocs <- [erlang:system_info({allocator,A})],
Allocs =/= false,
{_,N,Props} <- Allocs].
format_alloc(Alloc, Props) ->
%% {versions,_,_} is implicitly deleted in order to allow the use of the
%% orddict api, and never really having come across a case where it was
%% useful to know.
[{K, format_blocks(Alloc, K, V)} || {K, V} <- lists:sort(Props)].
format_blocks(_, _, []) ->
[];
format_blocks(Alloc, Key, [{blocks, L} | List]) when is_list(L) ->
%% OTP-22 introduces carrier migrations across types, and OTP-23 changes the
%% format of data reported to be a bit richer; however it's not compatible
%% with most calculations made for this library.
%% So what we do here for `blocks' is merge all the info into the one the
%% library expects (`blocks' and `blocks_size'), then keep the original
%% one in case it is further needed.
%% There were further changes to `mbcs_pool' changing `foreign_blocks',
%% `blocks' and `blocks_size' into just `blocks' with a proplist, so we're breaking
%% up to use that one too.
%% In the end we go from `{blocks, [{Alloc, [...]}]}' to:
%% - `{blocks, ...}' (4-tuple in mbcs and sbcs, 2-tuple in mbcs_pool)
%% - `{blocks_size, ...}' (4-tuple in mbcs and sbcs, 2-tuple in mbcs_pool)
%% - `{foreign_blocks, [...]}' (just append lists =/= `Alloc')
%% - `{raw_blocks, [...]}' (original value)
Foreign = lists:filter(fun({A, _Props}) -> A =/= Alloc end, L),
Type = case Key of
mbcs_pool -> int;
_ -> quadruple
end,
MergeF = fun(K) ->
fun({_A, Props}, Acc) ->
case lists:keyfind(K, 1, Props) of
{K,Cur,Last,Max} ->
{AccCur, AccLast, AccMax} = Acc,
{AccCur + Cur, AccLast + Last, AccMax + Max};
{K,V} -> Acc+V
end
end
end,
%% Since tuple sizes change, hack around it using tuple_to_list conversion
%% and set the accumulator to a list so it defaults to not putting anything
{Blocks, BlocksSize} = case Type of
int ->
{{blocks, lists:foldl(MergeF(count), 0, L)},
{blocks_size, lists:foldl(MergeF(size), 0, L)}};
quadruple ->
{list_to_tuple([blocks | tuple_to_list(lists:foldl(MergeF(count), {0,0,0}, L))]),
list_to_tuple([blocks_size | tuple_to_list(lists:foldl(MergeF(size), {0,0,0}, L))])}
end,
[Blocks, BlocksSize, {foreign_blocks, Foreign}, {raw_blocks, L}
| format_blocks(Alloc, Key, List)];
format_blocks(Alloc, Key, [H | T]) ->
[H | format_blocks(Alloc, Key, T)].
%% @doc returns a dump of all allocator settings and values modified
%% depending on the argument.
%% <ul>
%% <li>`types' report the settings and accumulated values for each
%% allocator type. This is useful when looking for anomalies
%% in the system as a whole and not specific instances.</li>
%% </ul>
-spec allocators(types) -> [allocdata_types(term())].
allocators(types) ->
allocators_types(alloc(), []).
allocators_types([{{Type,No},Vs}|T], As) ->
case lists:keytake(Type, 1, As) of
false ->
allocators_types(T,[{Type,[No],sort_values(Type, Vs)}|As]);
{value,{Type,Nos,OVs},NAs} ->
MergedValues = merge_values(sort_values(Type, Vs),OVs),
allocators_types(T,[{Type,[No|Nos],MergedValues}|NAs])
end;
allocators_types([], As) ->
[{{Type,Nos},Vs} || {Type, Nos, Vs} <- As].
merge_values([{Key,Vs}|T1], [{Key,OVs}|T2]) when Key =:= memkind ->
[{Key, merge_values(Vs, OVs)} | merge_values(T1, T2)];
merge_values([{Key,Vs}|T1], [{Key,OVs}|T2]) when Key =:= calls;
Key =:= fix_types;
Key =:= sbmbcs;
Key =:= mbcs;
Key =:= mbcs_pool;
Key =:= sbcs;
Key =:= status ->
[{Key,lists:map(
fun({{K,MV1,V1}, {K,MV2,V2}}) ->
%% Merge the MegaVs + Vs into one
V = MV1 * 1000000 + V1 + MV2 * 1000000 + V2,
{K, V div 1000000, V rem 1000000};
({{K,V1}, {K,V2}}) when K =:= segments_watermark ->
%% We take the maximum watermark as that is
%% a value that we can use somewhat. Ideally
%% maybe the average should be used, but the
%% value is very rarely important so leave it
%% like this for now.
{K, lists:max([V1,V2])};
({{K,V1}, {K,V2}}) when K =:= foreign_blocks; K =:= raw_blocks ->
%% foreign blocks are just merged as a bigger list.
{K, V1++V2};
({{K,V1}, {K,V2}}) ->
{K, V1 + V2};
({{K,C1,L1,M1}, {K,C2,L2,M2}}) ->
%% Merge the Curr, Last, Max into one
{K, C1+C2, L1+L2, M1+M2}
end, lists:zip(Vs,OVs))} | merge_values(T1,T2)];
merge_values([{Type,_Vs}=E|T1], T2) when Type =:= mbcs_pool ->
%% For values never showing up in instance 0 but in all other
[E|merge_values(T1,T2)];
merge_values(T1, [{Type,_Vs}=E|T2]) when Type =:= fix_types ->
%% For values only showing up in instance 0
[E|merge_values(T1,T2)];
merge_values([E|T1], [E|T2]) ->
%% For values that are constant
[E|merge_values(T1,T2)];
merge_values([{options,_Vs1}|T1], [{options,_Vs2} = E|T2]) ->
%% Options change a but in between instance 0 and the other,
%% We show the others as they are the most interesting.
[E|merge_values(T1,T2)];
merge_values([],[]) ->
[].
sort_values(mseg_alloc, Vs) ->
{value, {memkind, MemKindVs}, OVs} = lists:keytake(memkind, 1, Vs),
lists:sort([{memkind, lists:sort(MemKindVs)} | OVs]);
sort_values(_Type, Vs) ->
lists:sort(Vs).
%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Snapshot handling %%%
%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Take a new snapshot of the current memory allocator statistics.
%% The snapshot is stored in the process dictionary of the calling process,
%% with all the limitations that it implies (i.e. no garbage-collection).
%% To unsert the snapshot, see {@link snapshot_clear/0}.
-spec snapshot() -> snapshot() | undefined.
snapshot() ->
put(recon_alloc_snapshot, snapshot_int()).
%% @doc clear the current snapshot in the process dictionary, if present,
%% and return the value it had before being unset.
%% @end
%% Maybe we should use erlang:delete(Key) here instead?
-spec snapshot_clear() -> snapshot() | undefined.
snapshot_clear() ->
put(recon_alloc_snapshot, undefined).
%% @doc print a dump of the current snapshot stored by {@link snapshot/0}
%% Prints `undefined' if no snapshot has been taken.
-spec snapshot_print() -> ok.
snapshot_print() ->
io:format("~p.~n",[snapshot_get()]).
%% @doc returns the current snapshot stored by {@link snapshot/0}.
%% Returns `undefined' if no snapshot has been taken.
-spec snapshot_get() -> snapshot() | undefined.
snapshot_get() ->
get(recon_alloc_snapshot).
%% @doc save the current snapshot taken by {@link snapshot/0} to a file.
%% If there is no current snapshot, a snapshot of the current allocator
%% statistics will be written to the file.
-spec snapshot_save(Filename) -> ok when
Filename :: file:name().
snapshot_save(Filename) ->
Snapshot = case snapshot_get() of
undefined ->
snapshot_int();
Snap ->
Snap
end,
case file:write_file(Filename,io_lib:format("~p.~n",[Snapshot])) of
ok -> ok;
{error,Reason} ->
erlang:error(Reason,[Filename])
end.
%% @doc load a snapshot from a given file. The format of the data in the
%% file can be either the same as output by {@link snapshot_save/1},
%% or the output obtained by calling
%% `{erlang:memory(),[{A,erlang:system_info({allocator,A})} || A <- erlang:system_info(alloc_util_allocators)++[sys_alloc,mseg_alloc]]}.'
%% and storing it in a file.
%% If the latter option is taken, please remember to add a full stop at the end
%% of the resulting Erlang term, as this function uses `file:consult/1' to load
%% the file.
%%
%% Example usage:
%%
%%```On target machine:
%% 1> recon_alloc:snapshot().
%% undefined
%% 2> recon_alloc:memory(used).
%% 18411064
%% 3> recon_alloc:snapshot_save("recon_snapshot.terms").
%% ok
%%
%% On other machine:
%% 1> recon_alloc:snapshot_load("recon_snapshot.terms").
%% undefined
%% 2> recon_alloc:memory(used).
%% 18411064'''
%%
-spec snapshot_load(Filename) -> snapshot() | undefined when
Filename :: file:name().
snapshot_load(Filename) ->
{ok,[Terms]} = file:consult(Filename),
Snapshot =
case Terms of
%% We handle someone using
%% {erlang:memory(),
%% [{A,erlang:system_info({allocator,A})} ||
%% A <- erlang:system_info(alloc_util_allocators)++[sys_alloc,mseg_alloc]]}
%% to dump data.
{M,[{Alloc,_D}|_] = Allocs} when is_atom(Alloc) ->
{M,[{{A,N},lists:sort(proplists:delete(versions,Props))} ||
{A,Instances = [_|_]} <- Allocs,
{_, N, Props} <- Instances]};
%% We assume someone used recon_alloc:snapshot() to store this one
{M,Allocs} ->
{M,[{AN,lists:sort(proplists:delete(versions,Props))} ||
{AN, Props} <- Allocs]}
end,
put(recon_alloc_snapshot,Snapshot).
%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Handling of units %%%
%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc set the current unit to be used by recon_alloc. This effects all
%% functions that return bytes.
%%
%% Eg.
%% ```1> recon_alloc:memory(used,current).
%% 17548752
%% 2> recon_alloc:set_unit(kilobyte).
%% undefined
%% 3> recon_alloc:memory(used,current).
%% 17576.90625'''
%%
-spec set_unit(byte | kilobyte | megabyte | gigabyte) -> ok.
set_unit(byte) ->
put(recon_alloc_unit,undefined);
set_unit(kilobyte) ->
put(recon_alloc_unit,1024);
set_unit(megabyte) ->
put(recon_alloc_unit,1024*1024);
set_unit(gigabyte) ->
put(recon_alloc_unit,1024*1024*1024).
conv({Mem,Allocs} = D) ->
case get(recon_alloc_unit) of
undefined ->
D;
Factor ->
{conv_mem(Mem,Factor),conv_alloc(Allocs,Factor)}
end.
conv_mem(Mem,Factor) ->
[{T,M / Factor} || {T,M} <- Mem].
conv_alloc([{{sys_alloc,_I},_Props} = Alloc|R], Factor) ->
[Alloc|conv_alloc(R,Factor)];
conv_alloc([{{mseg_alloc,_I} = AI,Props}|R], Factor) ->
MemKind = orddict:fetch(memkind,Props),
Status = orddict:fetch(status,MemKind),
{segments_size,Curr,Last,Max} = lists:keyfind(segments_size,1,Status),
NewSegSize = {segments_size,Curr/Factor,Last/Factor,Max/Factor},
NewStatus = lists:keyreplace(segments_size,1,Status,NewSegSize),
NewProps = orddict:store(memkind,orddict:store(status,NewStatus,MemKind),
Props),
[{AI,NewProps}|conv_alloc(R,Factor)];
conv_alloc([{AI,Props}|R], Factor) ->
FactorFun = fun({T,Curr}) when
T =:= blocks_size; T =:= carriers_size ->
{T,Curr/Factor};
({T,Curr,Last,Max}) when
T =:= blocks_size; T =:= carriers_size;
T =:= mseg_alloc_carriers_size;
T =:= sys_alloc_carriers_size ->
{T,Curr/Factor,Last/Factor,Max/Factor};
(T) ->
T
end,
NewMbcsProp = [FactorFun(Prop) || Prop <- orddict:fetch(mbcs,Props)],
NewSbcsProp = [FactorFun(Prop) || Prop <- orddict:fetch(sbcs,Props)],
NewProps = orddict:store(sbcs,NewSbcsProp,
orddict:store(mbcs,NewMbcsProp,Props)),
case orddict:find(mbcs_pool,Props) of
error ->
[{AI,NewProps}|conv_alloc(R,Factor)];
{ok,MbcsPoolProps} ->
NewMbcsPoolProp = [FactorFun(Prop) || Prop <- MbcsPoolProps],
NewPoolProps = orddict:store(mbcs_pool,NewMbcsPoolProp,NewProps),
[{AI,NewPoolProps}|conv_alloc(R,Factor)]
end;
conv_alloc([],_Factor) ->
[].
%%%%%%%%%%%%%%%
%%% Private %%%
%%%%%%%%%%%%%%%
%% Sort on small usage vs large size.
%% The weight cares about both the sbcs and mbcs values, and also
%% returns a proplist of possibly interesting values.
weighed_values({SbcsBlockSize, SbcsCarrierSize},
{MbcsBlockSize, MbcsCarrierSize}) ->
SbcsUsage = usage(SbcsBlockSize, SbcsCarrierSize),
MbcsUsage = usage(MbcsBlockSize, MbcsCarrierSize),
SbcsWeight = (1.00 - SbcsUsage)*SbcsCarrierSize,
MbcsWeight = (1.00 - MbcsUsage)*MbcsCarrierSize,
Weight = SbcsWeight + MbcsWeight,
{Weight, [{sbcs_usage, SbcsUsage},
{mbcs_usage, MbcsUsage},
{sbcs_block_size, SbcsBlockSize},
{sbcs_carriers_size, SbcsCarrierSize},
{mbcs_block_size, MbcsBlockSize},
{mbcs_carriers_size, MbcsCarrierSize}]}.
%% Returns the `BlockSize/CarrierSize' as a 0.0 -> 1.0 percentage,
%% but also takes 0/0 to be 100% to make working with sorting and
%% weights simpler.
usage(0,0) -> 1.00;
usage(0.0,0.0) -> 1.00;
%usage(N,0) -> ???;
usage(Block,Carrier) -> Block/Carrier.
%% Calculation for the average of blocks being used.
average_calc([]) ->
[];
average_calc([{{Instance,Type,count},Ct},{{Instance,Type,size},Size}|Rest]) ->
case {Size,Ct} of
{_,0} when Size == 0 -> [{Instance, Type, 0} | average_calc(Rest)];
_ -> [{Instance,Type,Size/Ct} | average_calc(Rest)]
end.
%% Regrouping/merging values together in proplists
average_group([]) -> [];
average_group([{Instance,Type1,N},{Instance,Type2,M} | Rest]) ->
[{Instance,[{Type1,N},{Type2,M}]} | average_group(Rest)].
%% Get the total carrier size
container_size(Props, Keyword, Container) ->
Sbcs = container_value(Props, Keyword, sbcs, Container),
Mbcs = container_value(Props, Keyword, mbcs, Container),
Sbcs+Mbcs.
container_value(Props, Keyword, Type, Container)
when is_atom(Keyword) ->
container_value(Props, key2pos(Keyword), Type, Container);
container_value(Props, Pos, mbcs = Type, Container)
when Pos == ?CURRENT_POS,
((Container =:= blocks) or (Container =:= blocks_size)
or (Container =:= carriers) or (Container =:= carriers_size))->
%% We include the mbcs_pool into the value for mbcs.
%% The mbcs_pool contains carriers that have been abandoned
%% by the specific allocator instance and can therefore be
%% grabbed by another instance of the same type.
%% The pool was added in R16B02 and enabled by default in 17.0.
%% See erts/emulator/internal_docs/CarrierMigration.md in
%% Erlang/OTP repo for more details.
Pool = case proplists:get_value(mbcs_pool, Props) of
PoolProps when PoolProps =/= undefined ->
element(Pos,lists:keyfind(Container, 1, PoolProps));
_ -> 0
end,
TypeProps = proplists:get_value(Type, Props),
Pool + element(Pos,lists:keyfind(Container, 1, TypeProps));
container_value(Props, Pos, Type, Container)
when Type =:= sbcs; Type =:= mbcs ->
TypeProps = proplists:get_value(Type, Props),
element(Pos,lists:keyfind(Container, 1, TypeProps)).
%% Create a new snapshot
snapshot_int() ->
{erlang:memory(),allocators()}.
%% If no snapshot has been taken/loaded then we use current values
snapshot_get_int() ->
case snapshot_get() of
undefined ->
conv(snapshot_int());
Snapshot ->
conv(Snapshot)
end.
%% Get the alloc part of a snapshot
alloc() ->
{_Mem,Allocs} = snapshot_get_int(),
Allocs.
alloc(Type) ->
[{{T,Instance},Props} || {{T,Instance},Props} <- alloc(),
lists:member(T,Type)].
%% Get only alloc_util allocs
util_alloc() ->
alloc(?UTIL_ALLOCATORS).
key2pos(current) ->
?CURRENT_POS;
key2pos(max) ->
?MAX_POS.