instrument(3)

πŸ“¦
version 29.0.6 Β· 3 versions
, in ubuntu stonking

πŸ’– instrument β€” bestie, we're doing runtime archaeology πŸ’–

okay so first things first, this is the manual page for the instrument module in Erlang. it lives in the runtime_tools part of the world and honestly? it's giving detective energy. put your hair up, we're going in πŸ”βœ¨


🌸 NAME 🌸

instrument β€” Analysis and Utility Functions for Instrumentation πŸŽ€

like. it's the module you call when you wanna know where your memory went. that's the whole vibe. πŸ’…


✨ DESCRIPTION ✨

so the instrument module has your back if you wanna study the resource usage inside an Erlang runtime system. and rn? the only resource it can study is memory allocation. that's it, that's the whole menu. πŸ“

NOTE, and bestie pls read this one: since this module literally goes poking around in the internal details of the runtime system, it can differ a lot from one version to the next. the maintainers make no compatibility guarantees in this module. fr fr. it may just change on you. 😭

πŸ‘€ See Also

erts_alloc(3), erl(1)


🧚 DATA TYPES 🧚

allocation_origin()

allocation_origin() :: atom() | mfa() | pid() | port().

allocation_summary()

allocation_summary() ::
          {HistogramStart :: non_neg_integer(),
           UnscannedSize :: non_neg_integer(),
           Allocations ::
               #{Origin :: allocation_origin() =>
                     #{Type :: atom() => block_histogram()}}}.

it's a summary of allocated block sizes (and yes, headers count too, they're included) grouped up by their Origin and Type. slay.

Origin is basically which NIF or driver allocated the blocks β€” or 'system' if it just couldn't be figured out. awkward but honest. πŸ™ˆ

Type is the allocation category those blocks belong to, like db_term, message or binary. these categories match the ones you'll find in erl_alloc.types. πŸ’«

and if one or more carriers couldn't be scanned all the way through without messing with the responsiveness of the system, then UnscannedSize is how many bytes had to get skipped. we don't ruin the vibe for data, periodt.

block_histogram()

block_histogram() :: tuple().

a histogram of block sizes where each interval's upper bound is twice as high as the one before it. it's exponential, it's iconic, it's giving growth mindset. 🌱

the upper bound of the first interval comes from whichever function returned the histogram, and the last interval has no upper bound at all. no cap, it just goes forever. πŸ‘‘

so for example, the histogram below has 40 (message) blocks chillin' between 128-256 bytes, 78 blocks between 256-512 bytes, 2 blocks between 512-1024 bytes, and 2 blocks between 1-2KB:

> instrument:allocations(#{ histogram_start => 128, histogram_width => 15 }).
{ok, {128, 0, #{ message => {0,40,78,2,2,0,0,0,0,0,0,0,0,0,0}, ... } }}

look at that lil tuple go. πŸ¦‹

carrier_info_list()

carrier_info_list() ::
          {HistogramStart :: non_neg_integer(),
           Carriers ::
               [{AllocatorType :: atom(),
                 InPool :: boolean(),
                 TotalSize :: non_neg_integer(),
                 UnscannedSize :: non_neg_integer(),
                 Allocations ::
                     [{Type :: atom(),
                       Count :: non_neg_integer(),
                       Size :: non_neg_integer()}],
                 FreeBlocks :: block_histogram()}]}.

breaking it down because we're thorough girlies:

  • AllocatorType β€” the type of allocator that's employing this carrier.
  • InPool β€” whether the carrier is in the migration pool or not. yes or no bestie.
  • TotalSize β€” the total size of the carrier, header included.
  • Allocations β€” a summary of the allocated blocks living in the carrier. and heads up: carriers can contain multiple different block types when carrier pools are shared between different allocator types (go read the erts_alloc documentation for the deets).
  • FreeBlocks β€” a histogram of the free block sizes in the carrier. the empties. the potential. ✨
  • and if the carrier couldn't be scanned in full without harming the responsiveness of the system, UnscannedSize is the number of bytes that had to be skipped. same deal as before.

πŸ”₯ FUNCTIONS πŸ”₯

allocations(Options) πŸ’…

allocations(Options) -> {ok, Result} | {error, Reason}
                     when
                         Result :: allocation_summary(),
                         Reason :: not_enabled,
                         Options ::
                             #{scheduler_ids => [non_neg_integer()],
                               allocator_types => [atom()],
                               histogram_start => pos_integer(),
                               histogram_width => pos_integer(),
                               flags =>
                                   [per_process | per_port | per_mfa]}.

this one returns a summary of all the tagged allocations in the system, and you can optionally filter by allocator type and scheduler id. chef's kiss. πŸ‘‘

by default, only binaries and allocations made by NIFs and drivers get tagged β€” but you can configure that on a per-allocator basis with the +M<S>atags emulator option. so you can have more.

if the allocator types you specified aren't enabled, the call just fails with {error, not_enabled}. respectfully declined. 🚫

the following options can be used:

  • allocator_types β€” the allocator types that will be searched. specifying a specific allocator type may lead to strange results when carrier migration between different allocator types has been enabled: you might see unexpected types (like, say, process heaps when you're out here searching binary_alloc), or fewer blocks than you expected if the carriers those blocks are on got migrated out to an allocator of a different type. carrier migration said surprise. 😭 defaults to all alloc_util allocators.
  • scheduler_ids β€” the scheduler ids whose allocator instances will be searched. a scheduler id of 0 refers to the global instance that isn't tied to any particular scheduler. defaults to all schedulers and the global instance. everyone's invited. πŸŽ€
  • histogram_start β€” the upper bound of the first interval in the allocated block size histograms. defaults to 128.
  • histogram_width β€” the number of intervals in the allocated block size histograms. defaults to 18.
  • flags β€” controls how the output gets grouped, like showing allocations on a per-process basis (when it's possible) instead of only a NIF/driver basis. defaults to [].

example πŸ“

> instrument:allocations(#{ histogram_start => 128, histogram_width => 15 }).
{ok,{128,0,
     #{udp_inet =>
           #{driver_event_state => {0,0,0,0,0,0,0,0,0,1,0,0,0,0,0}},
       system =>
           #{heap => {0,0,0,0,20,4,2,2,2,3,0,1,0,0,1},
             db_term => {271,3,1,52,80,1,0,0,0,0,0,0,0,0,0},
             code => {0,0,0,5,3,6,11,22,19,20,10,2,1,0,0},
             binary => {18,0,0,0,7,0,0,1,0,0,0,0,0,0,0},
             message => {0,40,78,2,2,0,0,0,0,0,0,0,0,0,0},
             ... }
       spawn_forker =>
           #{driver_select_data_state =>
                 {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0}},
       ram_file_drv => #{drv_binary => {0,0,0,0,0,0,1,0,0,0,0,0,0,0,0}},
       prim_file =>
           #{process_specific_data => {2,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
             nif_trap_export_entry => {0,4,0,0,0,0,0,0,0,0,0,0,0,0,0},
             monitor_extended => {0,1,0,0,0,0,0,0,0,0,0,0,0,0,0},
             drv_binary => {0,0,0,0,0,0,1,0,3,5,0,0,0,1,0},
             binary => {0,4,0,0,0,0,0,0,0,0,0,0,0,0,0}},
       prim_buffer =>
           #{nif_internal => {0,4,0,0,0,0,0,0,0,0,0,0,0,0,0},
             binary => {0,4,0,0,0,0,0,0,0,0,0,0,0,0,0}}}}}

carriers(Options) πŸŽ€

carriers(Options) -> {ok, Result} | {error, Reason}
                  when
                      Result :: carrier_info_list(),
                      Reason :: not_enabled,
                      Options ::
                          #{scheduler_ids => [non_neg_integer()],
                            allocator_types => [atom()],
                            histogram_start => pos_integer(),
                            histogram_width => pos_integer()}.

this one returns a summary of all the carriers in the system, optionally filtered by allocator type and scheduler id. the whole carrier ecosystem, on display. 🌈

if the specified allocator types aren't enabled, the call fails with {error, not_enabled}. same energy as above.

the following options can be used:

  • allocator_types β€” the allocator types that will be searched. defaults to all alloc_util allocators.
  • scheduler_ids β€” the scheduler ids whose allocator instances will be searched. a scheduler id of 0 refers to the global instance that's not tied to any particular scheduler. defaults to all schedulers and the global instance.
  • histogram_start β€” the upper bound of the first interval in the free block size histograms. defaults to 512.
  • histogram_width β€” the number of intervals in the free block size histograms. defaults to 14.
note that histogram_start here defaults to 512 and histogram_width to 14, while in allocations/1 they default to 128 and 18. just so you don't get got. πŸ‘€

example ✨

> instrument:carriers(#{ histogram_start => 512, histogram_width => 8 }).
{ok,{512,
     [{driver_alloc,false,262144,0,
                    [{driver_alloc,1,32784}],
                    {0,0,0,0,0,0,0,1}},
      {binary_alloc,false,32768,0,
                    [{binary_alloc,15,4304}],
                    {3,0,0,0,1,0,0,0}},
      {...}|...]}}

look at those carriers. in a pool. being counted. we love to see it. πŸ¦‹


aaaand that's the page bestie!! πŸ’– quick reminder before you go: this module inspects internal details of the runtime system so it can change hella between versions and there are no compatibility guarantees. it's not being rude, it's just built different. 😭

this iconic lil doc was brought to you by Ericsson AB, shipping with runtime_tools 2.4, and it's the instrument(3) page in the Erlang module definition set. go forth, profile responsibly, and stay slay. πŸ’…πŸŽ€βœ¨

πŸ“¦ where this page comes from