Project: Tarantool (App Definition and Development / Database)
File: 5187-luajit-metrics.md

Status*: In progress Start date*: 17-07-2020 Authors*: Sergey Kaplun @Buristan skaplun@tarantool.org, Igor Munkin @igormunkin imun@tarantool.org, Sergey Ostanevich @sergos sergos@tarantool.org Issues*: LuaJIT metrics provide extra information about the Lua state. They consist of GC metrics (overall amount of objects and memory usage), JIT stats (both related to the compiled traces and the engine itself), string hash hits/misses. One can be curious about their application performance. We are going to provide various metrics about the several platform subsystems behaviour. GC pressure produced by user code can weight down all application performance. Irrelevant traces compiled by the JIT engine can just burn CPU time with no benefits as a result. String hash collisions can lead to DoS caused by a single request. All these metrics should be well monitored by users wanting to improve the performance of their application. The additional header <lmisclib.h> is introduced to extend the existing LuaJIT C API with new interfaces. The first function provided via this header is the following: ```c / API for obtaining various platform metrics. / LUAMISCAPI void luaMmetrics(luaState *L, struct luamMetrics *metrics); ``` This function fills the structure pointed to by `metrics` with the corresponding metrics related to Lua state anchored to the given coroutine `L`. The `struct luam_Metrics` has the following definition: ```c struct luam_Metrics { /* Number of strings being interned (i.e. the string with the same payload is found, so a new one is not created/allocated). */ sizet strhashhit; / Total number of strings allocations during the platform lifetime. / sizet strhashmiss; / Amount of allocated string objects. / sizet gcstrnum; / Amount of allocated table objects. / sizet gctabnum; / Amount of allocated udata objects. / sizet gcudatanum; / Amount of allocated cdata objects. / sizet gccdatanum; / Memory currently allocated. / sizet gctotal; / Total amount of freed memory. / sizet gcfreed; / Total amount of allocated memory. / sizet gcallocated; / Count of incremental GC steps per state. / sizet gcsteps_pause; sizet gcsteps_propagate; sizet gcsteps_atomic; sizet gcsteps_sweepstring; sizet gcsteps_sweep; sizet gcsteps_finalize; /* Overall number of snap restores (amount of guard assertions leading to stopping trace executions). */ sizet jitsnap_restore; / Overall number of abort traces. / sizet jittrace_abort; / Total size of all allocated machine code areas. / sizet jitmcode_size; / Amount of JIT traces. / unsigned int jittracenum; }; ``` Couple of words about how metrics are collected: `strhash_*` -- whenever the string with the same payload is found, so a new one is not created/allocated, there is incremented `strhash_hit` counter, if a new one string created/allocated then `strhash_miss` is incremented instead. `gc*num`, `jittrace_num` -- corresponding counter is incremented whenever a new object is allocated. When object is collected by GC its counter is decremented. `gctotal`, `gcallocated`, `gc_freed` -- any time when allocation function is called `gcallocated` and/or `gcfreed` is increased and `gc_total` is increased when memory is allocated or reallocated, is decreased when memory is

`gcsteps*` -- corresponding counter is incremented whenever Garbage Collector starts to execute an incremental step of garbage collection. `jitsnaprestore` -- whenever JIT machine exits from the trace and restores interpreter state `jitsnaprestore` counter is incremented. `jittraceabort` -- whenever JIT compiler can't record the trace in case NYI BC or builtins this counter is incremented. `jitmcodesize` -- whenever new MCode area is allocated `jitmcodesize` is increased at corresponding size in bytes. Sets to 0 when all mcode area is freed. When a trace is collected by GC this value doesn't change. This area will be reused later for other traces. MCode area is linked with `jit_State` not with trace by itself. Traces just reserve MCode area that needed. All metrics are collected throughout the platform uptime. These metrics increase monotonically and can overflow: `strhash_hit` `strhash_miss` `gc_freed` `gc_allocated` `gcstepspause` `gcstepspropagate` `gcstepsatomic` `gcstepssweepstring` `gcstepssweep` `gcstepsfinalize` `jitsnaprestore` `jittraceabort` They make sense only with comparing with their value from a previous `luaM_metrics()` call. There is also a complement introduced for Lua space -- `misc.getmetrics()`. This function is just a wrapper for `luaM_metrics()` returning a Lua table with the similar metrics. All returned values are presented as numbers with cast to double, so there is a corresponding precision loss. Function usage is quite simple: ``` $ ./src/tarantool Tarantool 2.5.0-267-gbf047ad44 type 'help' for interactive help tarantool> misc.getmetrics() gc_freed: 2245853 strhash_hit: 53965 gcstepsatomic: 6 strhash_miss: 6879 gcstepssweepstring: 17920 gc_strnum: 5759 gc_tabnum: 1813 gc_cdatanum: 89 jitsnaprestore: 0 gc_total: 1370812 gc_udatanum: 17 gcstepsfinalize: 0 gc_allocated: 3616665 jittracenum: 0 gcstepssweep: 297 jittraceabort: 0 jitmcodesize: 0 gcstepspropagate: 10181 gcstepspause: 7 ... ``` This section describes small example of metrics usage. For example amount of `strhash_misses` can be shown for tracking of new string objects allocations. For example if we add code like: ```lua local function shardedstoragefunc(storagename, funcname) return 'shardedstorage.storages.' .. storagename .. '.' .. func_name end ``` increase in slope curve of `strhash_misses` means, that after your changes there are more new strings allocating at runtime. Of course slope curve of `strhashmisses` should be less than slope curve of `strhashhits`. Slope curves of `gcfreed` and `gcallocated` can be used for analysis of GC pressure of your application (less is better). Also we can check some hacky optimization with these metrics. For example let's assume that we have this code snippet: ```lua local old_metrics = misc.getmetrics() local t = {} for i = 1, 513 do t[i] = i end local new_metrics = misc.getmetrics() local diff = newmetrics.gcallocated - oldmetrics.gcallocated ``` `diff` equals to 18879 after running of this chunk. But if we change table initialization to ```lua local table_new = require "table.new" local old_metrics = misc.getmetrics() local t = table_new(513,0) for i = 1, 513 do t[i] = i end local new_metrics = misc.getmetrics() local diff = newmetrics.gcallocated - oldmetrics.gcallocated ``` `diff` shows us only 5895. Slope curves of `gcsteps*` can be used for tracking of GC pressure too. For long time observations you will see periodic increment for `gcsteps*` metrics -- for example longer period of `gcstepsatomic` increment is

Also additional amount of `gcstepspropagate` in one period can be used to indirectly estimate amount of objects. These values also correlate with the step multiplier of the GC. The amount of incremental steps can grow, but one step can process a small amount of objects. So these metrics should be considered together with GC setup. Amount of `gc_*num` is useful for control of memory leaks -- total amount of these objects should not growth nonstop (you can also track `gc_total` for this). Also `jitmcodesize` can be used for tracking amount of allocated memory for traces machine code. Slope curves of `jittraceabort` shows how many times trace hasn't been compiled when the attempt was made (less is better). Amount of `gctracenum` is shown how much traces was generated (usually more is better). And the last one -- `gcsnaprestores` can be used for estimation when LuaJIT is stop trace execution. If slope curves of this metric growth after changing old code it can mean performance degradation. Assumes that we have code like this: ```lua local function foo(i) return i <= 5 and i or tostring(i) end -- minstitch option needs to emulate nonstitching behaviour jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15") local sum = 0 local old_metrics = misc.getmetrics() for i = 1, 10 do sum = sum + foo(i) end local new_metrics = misc.getmetrics() local diff = newmetrics.jitsnaprestore - oldmetrics.jitsnaprestore ``` `diff` equals 3 (1 side exit on loop end, 2 side exits to the interpreter before trace gets hot and compiled) after this chunk of code. And now we decide to change `foo` function like this: ```lua local function foo(i) -- math.fmod is not yet compiled! return i <= 5 and i or math.fmod(i, 11) end ``` `diff` equals 6 (1 side exit on loop end, 2 side exits to the interpreter before trace gets hot and compiled an 3 side exits from the root trace could not get compiled) after the same chunk of code. Benchmarks were taken from repo: . Example of usage: ```bash /usr/bin/time -f"array3d %U" ./luajit $BENCH_DIR/array3d.lua 300 >/dev/null ``` Taking into account the measurement error ~ 2%, it can be said that there is no difference in the performance. Benchmark results after and before patch (less is better): ``` Benchmark | AFTER (s) | BEFORE (s) +--+-- array3d | 0.21 | 0.20 binary-trees | 3.30 | 3.24 chameneos | 2.86 | 2.99 coroutine-ring | 0.98 | 1.02 euler14-bit | 1.01 | 1.05 fannkuch | 6.74 | 6.81 fasta | 8.25 | 8.28 life | 0.47 | 0.46 mandelbrot | 2.65 | 2.68 mandelbrot-bit | 1.96 | 1.97 md5 | 1.58 | 1.54 nbody | 1.36 | 1.56 nsieve | 2.02 | 2.06 nsieve-bit | 1.47 | 1.50 nsieve-bit-fp | 4.37 | 4.60 partialsums | 0.54 | 0.55 pidigits-nogmp | 3.46 | 3.46 ray | 1.62 | 1.63 recursive-ack | 0.19 | 0.20 recursive-fib | 1.63 | 1.67 scimark-fft | 5.76 | 5.86 scimark-lu | 3.58 | 3.64 scimark-sor | 2.33 | 2.34 scimark-sparse | 4.88 | 4.93 series | 0.94 | 0.94 spectral-norm | 0.94 | 0.97 ```