API метрик LuaJIT (getmetrics)
Tarantool может возвращать метрики текущего экземпляра через Lua API или C API.
Получить значения метрик в виде таблицы.
Параметры: отсутствуют
Возвращает
Таблица
Пример: metrics_table = misc.getmetrics()
Таблица метрик содержит 19 значений. Все значения имеют тип number и являются результатом приведения к double,
поэтому возможна очень незначительная потеря точности. Значения, имена которых начинаются с gc_, связаны со
сборщиком мусора LuaJIT; более подробное описание сборщика мусора
можно найти на странице Lua-users wiki и в
презентации создателя Lua. Значения, имена которых
начинаются с jit_, связаны с "фазами" процесса
JIT-компиляции; более подробное описание фаз JIT можно найти на
cern.ch.
Значения, описанные как "монотонные", являются кумулятивными, то есть это "итог с момента начала всех операций", а не "с
момента последнего вызова getmetrics()". Возможно переполнение.
Поскольку многие значения монотонные, типичный анализ включает вызов getmetrics(), сохранение таблицы, повторный
вызов getmetrics() и сравнение таблицы с сохраненной. Разница — это "кривая наклона". Интересная кривая наклона – та,
которая показывает ускорение, например, разница между последним и предыдущим значением постоянно растет. Некоторые элементы
таблицы, описанные здесь, используются в примерах далее в этом разделе.
Имя | Содержание | Монотонный? |
|---|---|---|
| Количество байт выделенной памяти. | Да |
| Количество выделенных объектов | Нет |
| Количество байт освобожденной памяти. | Да |
| Количество шагов сборщика мусора, атомарные фазы, инкрементальные. | Да |
| Количество шагов сборщика мусора, финализация. | Да |
| Количество шагов сборщика мусора, паузы. | Да |
| Количество шагов сборщика мусора, распространение. | Да |
| Количество шагов сборщика мусора, фазы очистки (см. описание фазы очистки). | Да |
| Количество шагов сборщика мусора, фазы очистки для строк. | Да |
| Количество выделенных строковых объектов. | Нет |
| Количество выделенных объектов-таблиц. | Нет |
| Количество байт текущей выделенной памяти (обычно равно | Нет |
| Количество выделенных объектов | Нет |
| Общий размер всех выделенных областей машинного кода. | Нет |
| Общее количество восстановлений снимков, основанное на количестве проверок-утверждений, приводящих к остановке выполнения трасс (см. внешний руководство по Snap). | Да |
| Общее количество прерванных трасс. | Да |
| Количество JIT-трасс. | Нет |
| Количество интернированных строк: если строка с тем же значением найдена через хеш, новая не создается и не выделяется. | Да |
| Общее количество выделений строк за время работы платформы. | Да |
Функция Lua getmetrics() является оберткой для C-функции luaM_metrics().
C-программы могут подключить заголовочный файл libmisclib.h. Определения в libmisclib.h включают следующие строки:
struct luam_Metrics { /* имена, описанные ранее для Lua */ }LUAMISC_API void luaM_metrics(lua_State *L, struct luam_Metrics *metrics);
Имена членов struct luam_Metrics совпадают с именами значений таблицы getmetrics
в Lua. Типы данных всех членов struct luam_Metrics – size_t. Функция luaM_metrics() заполняет структуру *metrics
метриками, относящимися к состоянию Lua, привязанному к сопрограмме L.
Пример с C-программой
Пройдите руководство по хранимым процедурам на C. Замените пример easy.c на следующий:
#include "module.h"#include <lmisclib.h>int easy(box_function_ctx_t *ctx, const char *args, const char *args_end){lua_State *ls = luaT_state();struct luam_Metrics m;luaM_metrics(ls, &m);printf("allocated memory = %lu\n", m.gc_allocated);return 0;}
После возврата к клиенту и выполнения запросов вплоть до строки capi_connection:call('easy') вывод будет выглядеть примерно как
"allocated memory = 4431950", хотя число может отличаться.
Для отслеживания выделения новых строковых объектов:
function f()collectgarbage("collect")local oldm = misc.getmetrics()local table_of_strings = {}for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) endfor i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) endlocal newm = misc.getmetrics()print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)endf()
Результат, вероятно, будет следующим: gc_strnum diff = 1100, так как добавлено 1202 строки, но 101 из них – дубликаты;
strhash_miss_diff = 1100 – по той же причине; strhash_hit_diff = 101 плюс небольшая погрешность – по той же причине.
Слово вероятно использовано, потому что есть вероятность, что строки уже были выделены где-то ранее. Хорошим
признаком является ситуация, когда кривая наклона strhash_miss меньше кривой наклона strhash_hit.
Остальные значения gc_*num – gc_cdatanum, gc_tabnum, gc_udatanum – можно получить аналогичным образом. Любое из
значений gc_*num может быть полезно при поиске утечек памяти – общее количество этих объектов не должно постоянно расти.
Более общий способ поиска утечек памяти – отслеживание gc_total. Также jit_mcode_size можно использовать для контроля
объема памяти, выделенной для трасс машинного кода.
Для отслеживания влияния приложения на сборщик мусора (чем меньше, тем лучше):
function f()for i = 1, 10 do collectgarbage("collect") endlocal oldm = misc.getmetrics()local newm = misc.getmetrics()oldm = misc.getmetrics()collectgarbage("collect")newm = misc.getmetrics()print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)endf()
Результат: gc_allocated diff = 800, gc_freed diff = 800. Это показывает, что сам по себе local ... = getmetrics()
вызывает выделение памяти (поскольку создается таблица и производится присваивание), а также показывает, что при повторном
использовании имени переменной (в данном случае oldm) происходит освобождение памяти. Обычно освобождение не происходит
немедленно, но collectgarbage("collect") принудительно выполняет его, чтобы мы могли увидеть эффект.
Чтобы проверить, возможна ли оптимизация памяти при работе с таблицами:
function f()collectgarbage("collect")local oldm = misc.getmetrics()local t = {}for i = 1, 513 dot[i] = iendlocal newm = misc.getmetrics()local diff = newm.gc_allocated - oldm.gc_allocatedprint("diff = " .. diff)endf()
Результат покажет, что diff равен примерно 18000.
Теперь посмотрим, что произойдет при другом способе инициализации таблицы:
function f()local table_new = require "table.new"local oldm = misc.getmetrics()local t = table_new(513, 0)for i = 1, 513 dot[i] = iendlocal newm = misc.getmetrics()local diff = newm.gc_allocated - oldm.gc_allocatedprint("diff = " .. diff)endf()
Результат покажет, что diff равен примерно 6000.
Кривые наклона элементов gc_steps_* также можно использовать для контроля нагрузки на сборщик мусора. Во время
длительных операций значения gc_steps_* будут расти, но большие интервалы между увеличениями gc_steps_atomic –
хороший признак. Поскольку gc_steps_atomic увеличивается только один раз за цикл сборки мусора, этот показатель
отражает количество выполненных циклов сборки мусора.
Кроме того, по увеличениям значения gc_steps_propagate можно косвенно оценить количество объектов. Эти значения
также коррелируют с множителем шага сборщика мусора. Например,
количество инкрементальных шагов может расти, но в соответствии с конфигурацией множителя шага один шаг может
обработать лишь небольшое количество объектов. Поэтому эти метрики следует учитывать при настройке сборщика мусора.
Следующая функция позволяет бегло оценить, вызывает ли SQL-запрос значительную нагрузку:
function f()collectgarbage("collect")local oldm = misc.getmetrics()collectgarbage("collect")box.execute([[DROP TABLE _vindex;]])local newm = misc.getmetrics()print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)endf()
Вывод покажет, что метрики gc_steps_* существенно не отличаются от тех, которые были бы без вызова box.execute().
JIT-компиляторы "трассируют" код в поисках возможностей для компиляции. jit_trace_abort показывает, как часто
происходили неудачные попытки (чем меньше, тем лучше), а jit_trace_num – сколько трасс было сгенерировано с
момента последней очистки (обычно чем больше, тем лучше).
Следующая функция не содержит кода, который может вызвать проблемы для LuaJIT:
function f()jit.flush()for i = 1, 10 do collectgarbage("collect") endlocal oldm = misc.getmetrics()collectgarbage("collect")local sum = 0for i = 1, 57 dosum = sum + 57endfor i = 1, 10 do collectgarbage("collect") endlocal newm = misc.getmetrics()print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)endf()
Результат: trace_num = 1, trace_abort = 0. Отлично.
Следующая функция, по-видимому, содержит код, который может вызвать проблемы для LuaJIT:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")_G.globalthing = 5function f()jit.flush()collectgarbage("collect")local oldm = misc.getmetrics()collectgarbage("collect")local sum = 0for i = 1, box.space._vindex:count()+ _G.globalthing dobox.execute([[SELECT RANDOMBLOB(0);]])require('buffer').ibuf()_G.globalthing = _G.globalthing - 1endlocal newm = misc.getmetrics()print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)endf()
Результат: trace_num = от 2 до 4, trace_abort = 1. Это означает, что вместо одной потребовалось сгенерировать до
четырех трасс, и это означает, что что-то заставило LuaJIT отказаться от попыток. Дальнейшее трассирование
покажет, что проблема кроется не в подозрительных операторах внутри функции, а в вызове jit.opt.start.
(Изучение файла jit.dump может помочь в исследовании процесса компиляции трасс.)
Если кривые наклона метрики jit_snap_restore растут после изменений в существующем коде, это может означать, что LuaJIT
чаще останавливает выполнение трасс, что может свидетельствовать о снижении производительности.
Начнем со следующего кода:
function f()local function foo(i)return i <= 5 and i or tostring(i)end-- параметр minstitch эмулирует поведение без сшивкиjit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")local sum = 0local oldm = misc.getmetrics()for i = 1, 10 dosum = sum + foo(i)endlocal newm = misc.getmetrics()local diff = newm.jit_snap_restore - oldm.jit_snap_restoreprint("diff = " .. diff)endf()
Результат: diff = 3, так как есть один побочный выход при завершении цикла и два побочных выхода в интерпретатор до того,
как LuaJIT может решить, что фрагмент кода "горячий" (значение параметра hotloop по умолчанию равно 56 согласно
Running LuaJIT).
Теперь изменим только одну строку внутри функции local foo, и код примет вид:
function f()local function foo(i)-- math.fmod еще не компилируется!return i <= 5 and i or math.fmod(i, 11)end-- параметр minstitch эмулирует поведение без сшивкиjit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")local sum = 0local oldm = misc.getmetrics()for i = 1, 10 dosum = sum + foo(i)endlocal newm = misc.getmetrics()local diff = newm.jit_snap_restore - oldm.jit_snap_restoreprint("diff = " .. diff)endf()
Результат: diff будет больше, так как количество побочных выходов увеличилось. Таким образом, этот тест показывает,
что изменение кода повлияло на производительность.