tests FEATURE performance test
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f894e7d..46569a0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -205,6 +205,7 @@
     option(ENABLE_VALGRIND_TESTS "Build tests with valgrind" OFF)
     set(INTERNAL_DOCS NO)
 endif()
+option(ENABLE_PERF_TESTS "Build performance tests" OFF)
 option(ENABLE_COVERAGE "Build code coverage report from tests" OFF)
 option(ENABLE_FUZZ_TARGETS "Build target programs suitable for fuzzing with AFL" OFF)
 
@@ -250,6 +251,24 @@
     endif()
 endif()
 
+if(ENABLE_PERF_TESTS)
+    find_path(CALLGRIND_INCLUDE_DIR
+        NAMES
+        valgrind/callgrind.h
+        PATHS
+        /usr/include
+        /usr/local/include
+        /opt/local/include
+        /sw/include
+        ${CMAKE_INCLUDE_PATH}
+        ${CMAKE_INSTALL_PREFIX}/include)
+    if(CALLGRIND_INCLUDE_DIR)
+        set(HAVE_CALLGRIND 1)
+    else()
+        message(STATUS "Disabling callgrind macros in performance tests because of missing headers")
+    endif()
+endif()
+
 if(ENABLE_COVERAGE)
     gen_coverage_enable(${ENABLE_TESTS})
 endif()
@@ -343,7 +362,7 @@
 endif()
 
 # tests
-if(ENABLE_TESTS)
+if(ENABLE_TESTS OR ENABLE_PERF_TESTS)
     enable_testing()
     add_subdirectory(tests)
 endif()
diff --git a/README.md b/README.md
index 7bb139a..45ae6d7 100644
--- a/README.md
+++ b/README.md
@@ -217,6 +217,20 @@
 $ make test
 ```
 
+### Perf
+
+There is a performance measurement tool included that prints information about
+the time required to execute common use-cases of working with YANG instance data.
+
+To enable this test, use:
+```
+$ cmake -DENABLE_PERF_TESTS=ON ..
+```
+and to run the test with seeing its output:
+```
+ctest -V -R ly_perf
+```
+
 ### Code Coverage
 
 Based on the tests run, it is possible to generate code coverage report. But
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index f66b8be..1736abf 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -32,9 +32,14 @@
     endif()
 endfunction()
 
-add_subdirectory(style)
-add_subdirectory(plugins)
-add_subdirectory(utests)
-add_subdirectory(fuzz)
+if(ENABLE_TESTS)
+    add_subdirectory(style)
+    add_subdirectory(plugins)
+    add_subdirectory(utests)
+    add_subdirectory(fuzz)
+endif()
+if(ENABLE_PERF_TESTS)
+    add_subdirectory(perf)
+endif()
 
 set(format_sources ${format_sources} PARENT_SCOPE)
diff --git a/tests/perf/CMakeLists.txt b/tests/perf/CMakeLists.txt
new file mode 100644
index 0000000..b7f325c
--- /dev/null
+++ b/tests/perf/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(format_sources
+    ${format_sources}
+    ${CMAKE_CURRENT_SOURCE_DIR}/perf.c
+    PARENT_SCOPE)
+
+add_executable(ly_perf ${CMAKE_CURRENT_SOURCE_DIR}/perf.c $<TARGET_OBJECTS:yangobj>)
+set_target_properties(ly_perf PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
+target_link_libraries(ly_perf ${CMAKE_THREAD_LIBS_INIT} ${PCRE2_LIBRARIES} ${CMAKE_DL_LIBS} m)
+
+add_test(NAME ly_perf_1000 COMMAND ly_perf 1000 10)
+add_test(NAME ly_perf_100000 COMMAND ly_perf 100000 3)
diff --git a/tests/perf/perf.c b/tests/perf/perf.c
new file mode 100644
index 0000000..b14dc53
--- /dev/null
+++ b/tests/perf/perf.c
@@ -0,0 +1,786 @@
+/**
+ * @file perf.c
+ * @author Michal Vasko <mvasko@cesnet.cz>
+ * @brief performance tests
+ *
+ * Copyright (c) 2021 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#define _GNU_SOURCE
+
+#include <assert.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <sys/time.h>
+#include <time.h>
+
+#include "libyang.h"
+#include "tests_config.h"
+
+#ifdef HAVE_CALLGRIND
+# include <valgrind/callgrind.h>
+#endif
+
+#define TEMP_FILE "perf_tmp"
+
+/**
+ * @brief Test state structure.
+ */
+struct test_state {
+    const struct lys_module *mod;
+    uint32_t count;
+    struct lyd_node *data1;
+    struct lyd_node *data2;
+};
+
+typedef LY_ERR (*setup_cb)(const struct lys_module *mod, uint32_t count, struct test_state *state);
+
+typedef LY_ERR (*test_cb)(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end);
+
+/**
+ * @brief Single test structure.
+ */
+struct test {
+    const char *name;
+    setup_cb setup;
+    test_cb test;
+};
+
+/**
+ * @brief Get current time as timespec.
+ *
+ * @param[out] ts Timespect to fill.
+ */
+static void
+time_get(struct timespec *ts)
+{
+#ifdef CLOCK_MONOTONIC_RAW
+    clock_gettime(CLOCK_MONOTONIC_RAW, ts);
+#elif defined (CLOCK_MONOTONIC)
+    clock_gettime(CLOCK_MONOTONIC, ts);
+#elif defined (CLOCK_REALTIME)
+    /* no monotonic clock available, return realtime */
+    clock_gettime(CLOCK_REALTIME, ts);
+#else
+    int rc;
+    struct timeval tv;
+
+    gettimeofday(&tv, NULL);
+    ts->tv_sec = (time_t)tv.tv_sec;
+    ts->tv_nsec = 1000L * (long)tv.tv_usec;
+#endif
+}
+
+/**
+ * @brief Get the difference of 2 timespecs in microseconds.
+ *
+ * @param[in] ts1 Smaller (older) timespec.
+ * @param[in] ts2 Larger (later) timespec.
+ * @return Difference of timespecs in usec.
+ */
+static uint64_t
+time_diff(const struct timespec *ts1, const struct timespec *ts2)
+{
+    uint64_t usec_diff = 0;
+    int64_t nsec_diff;
+
+    assert(ts1->tv_sec <= ts2->tv_sec);
+
+    /* seconds diff */
+    usec_diff += (ts2->tv_sec - ts1->tv_sec) * 1000000;
+
+    /* nanoseconds diff */
+    nsec_diff = ts2->tv_nsec - ts1->tv_nsec;
+    usec_diff += nsec_diff ? nsec_diff / 1000 : 0;
+
+    return usec_diff;
+}
+
+/**
+ * @brief Create data tree with list instances.
+ *
+ * @param[in] mod Module of the top-level node.
+ * @param[in] offset Starting offset of the identifier number values.
+ * @param[in] count Number of list instances to create, with increasing identifier numbers.
+ * @param[out] data Created data.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+create_list_inst(const struct lys_module *mod, uint32_t offset, uint32_t count, struct lyd_node **data)
+{
+    LY_ERR ret;
+    uint32_t i;
+    char k1_val[32], k2_val[32], l_val[32];
+    struct lyd_node *list;
+
+    if ((ret = lyd_new_inner(NULL, mod, "cont", 0, data))) {
+        return ret;
+    }
+
+    for (i = 0; i < count; ++i) {
+        sprintf(k1_val, "%" PRIu32, i + offset);
+        sprintf(k2_val, "str%" PRIu32, i + offset);
+        sprintf(l_val, "l%" PRIu32, i + offset);
+
+        if ((ret = lyd_new_list(*data, NULL, "lst", 0, &list, k1_val, k2_val))) {
+            return ret;
+        }
+        if ((ret = lyd_new_term(list, NULL, "l", l_val, 0, NULL))) {
+            return ret;
+        }
+    }
+
+    return LY_SUCCESS;
+}
+
+/**
+ * @brief Execute a test.
+ *
+ * @param[in] setup Setup callback to call once.
+ * @param[in] test Test callback.
+ * @param[in] name Name of the test.
+ * @param[in] mod Module of testing data.
+ * @param[in] count Count of list instances, size of the testing data set.
+ * @param[in] tries Number of (re)tries of the test to get more accurate measurements.
+ * @return LY_ERR value.
+ */
+static LY_ERR
+exec_test(setup_cb setup, test_cb test, const char *name, const struct lys_module *mod, uint32_t count, uint32_t tries)
+{
+    LY_ERR ret;
+    struct timespec ts_start, ts_end;
+    struct test_state state = {0};
+    const uint32_t name_fixed_len = 37;
+    char str[name_fixed_len + 1];
+    uint32_t i, printed;
+    uint64_t time_usec = 0;
+
+    /* print test start */
+    printed = sprintf(str, "| %s ", name);
+    while (printed + 1 < name_fixed_len) {
+        printed += sprintf(str + printed, ".");
+    }
+    if (printed < name_fixed_len) {
+        printed += sprintf(str + printed, " ");
+    }
+    sprintf(str + printed, "|");
+    fputs(str, stdout);
+    fflush(stdout);
+
+    /* setup */
+    if ((ret = setup(mod, count, &state))) {
+        return ret;
+    }
+
+    /* test */
+    for (i = 0; i < tries; ++i) {
+        if ((ret = test(&state, &ts_start, &ts_end))) {
+            return ret;
+        }
+        time_usec += time_diff(&ts_start, &ts_end);
+    }
+    time_usec /= tries;
+
+    /* teardown */
+    lyd_free_siblings(state.data1);
+    lyd_free_siblings(state.data2);
+
+    /* print time */
+    printf(" %" PRIu64 ".%06" PRIu64 " s |\n", time_usec / 1000000, time_usec % 1000000);
+
+    return LY_SUCCESS;
+}
+
+static void
+TEST_START(struct timespec *ts)
+{
+    time_get(ts);
+
+#ifdef HAVE_CALLGRIND
+    CALLGRIND_START_INSTRUMENTATION;
+#endif
+}
+
+static void
+TEST_END(struct timespec *ts)
+{
+    time_get(ts);
+
+#ifdef HAVE_CALLGRIND
+    CALLGRIND_STOP_INSTRUMENTATION;
+#endif
+}
+
+/* TEST SETUP */
+static LY_ERR
+setup_basic(const struct lys_module *mod, uint32_t count, struct test_state *state)
+{
+    state->mod = mod;
+    state->count = count;
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+setup_data_single_tree(const struct lys_module *mod, uint32_t count, struct test_state *state)
+{
+    state->mod = mod;
+    state->count = count;
+
+    return create_list_inst(mod, 0, count, &state->data1);
+}
+
+static LY_ERR
+setup_data_same_trees(const struct lys_module *mod, uint32_t count, struct test_state *state)
+{
+    LY_ERR ret;
+
+    state->mod = mod;
+    state->count = count;
+
+    if ((ret = create_list_inst(mod, 0, count, &state->data1))) {
+        return ret;
+    }
+    if ((ret = create_list_inst(mod, 0, count, &state->data2))) {
+        return ret;
+    }
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+setup_data_no_same_trees(const struct lys_module *mod, uint32_t count, struct test_state *state)
+{
+    LY_ERR ret;
+
+    state->mod = mod;
+    state->count = count;
+
+    if ((ret = create_list_inst(mod, 0, count, &state->data1))) {
+        return ret;
+    }
+    if ((ret = create_list_inst(mod, count, count, &state->data2))) {
+        return ret;
+    }
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+setup_data_offset_tree(const struct lys_module *mod, uint32_t count, struct test_state *state)
+{
+    LY_ERR ret;
+
+    state->mod = mod;
+    state->count = count;
+
+    if ((ret = create_list_inst(mod, count, count, &state->data2))) {
+        return ret;
+    }
+
+    return LY_SUCCESS;
+}
+
+/* TEST CB */
+static LY_ERR
+test_create_new_text(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data = NULL;
+
+    TEST_START(ts_start);
+
+    if ((r = create_list_inst(state->mod, 0, state->count, &data))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(data);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_create_new_bin(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data = NULL;
+    uint32_t i, k2_len, l_len;
+    char k2_val[32], l_val[32];
+    struct lyd_node *list;
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_new_inner(NULL, state->mod, "cont", 0, &data))) {
+        return r;
+    }
+
+    for (i = 0; i < state->count; ++i) {
+        k2_len = sprintf(k2_val, "str%" PRIu32, i);
+        l_len = sprintf(l_val, "l%" PRIu32, i);
+
+        if ((r = lyd_new_list_bin(data, NULL, "lst", 0, &list, &i, sizeof i, k2_val, k2_len))) {
+            return r;
+        }
+        if ((r = lyd_new_term_bin(list, NULL, "l", l_val, l_len, 0, NULL))) {
+            return r;
+        }
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(data);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_create_path(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data = NULL;
+    uint32_t i;
+    char path[64], l_val[32];
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_new_inner(NULL, state->mod, "cont", 0, &data))) {
+        return r;
+    }
+
+    for (i = 0; i < state->count; ++i) {
+        sprintf(path, "/perf:cont/lst[k1='%" PRIu32 "'][k2='str%" PRIu32 "']/l", i, i);
+        sprintf(l_val, "l%" PRIu32, i);
+
+        if ((r = lyd_new_path(data, NULL, path, l_val, 0, NULL))) {
+            return r;
+        }
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(data);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+_test_parse(struct test_state *state, LYD_FORMAT format, ly_bool use_file, uint32_t print_options, uint32_t parse_options,
+        uint32_t validate_options, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR ret = LY_SUCCESS;
+    struct lyd_node *data = NULL;
+    char *buf = NULL;
+    struct ly_in *in = NULL;
+
+    if (use_file) {
+        if ((ret = lyd_print_path(TEMP_FILE, state->data1, format, print_options))) {
+            goto cleanup;
+        }
+        if ((ret = ly_in_new_filepath(TEMP_FILE, 0, &in))) {
+            goto cleanup;
+        }
+    } else {
+        if ((ret = lyd_print_mem(&buf, state->data1, format, print_options))) {
+            goto cleanup;
+        }
+        if ((ret = ly_in_new_memory(buf, &in))) {
+            goto cleanup;
+        }
+    }
+
+    TEST_START(ts_start);
+
+    if ((ret = lyd_parse_data(state->mod->ctx, NULL, in, format, parse_options, validate_options, &data))) {
+        goto cleanup;
+    }
+
+    TEST_END(ts_end);
+
+cleanup:
+    free(buf);
+    ly_in_free(in, 0);
+    lyd_free_siblings(data);
+    return ret;
+}
+
+static LY_ERR
+test_parse_xml_mem_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_XML, 0, LYD_PRINT_SHRINK, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_xml_mem_no_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_XML, 0, LYD_PRINT_SHRINK, LYD_PARSE_STRICT | LYD_PARSE_ONLY, 0, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_xml_file_no_validate_format(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_XML, 1, 0, LYD_PARSE_STRICT | LYD_PARSE_ONLY, 0, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_json_mem_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_JSON, 0, LYD_PRINT_SHRINK, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_json_mem_no_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_JSON, 0, LYD_PRINT_SHRINK, LYD_PARSE_STRICT | LYD_PARSE_ONLY, 0, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_json_file_no_validate_format(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_JSON, 1, 0, LYD_PARSE_STRICT | LYD_PARSE_ONLY, 0, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_lyb_mem_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_LYB, 0, LYD_PRINT_SHRINK, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_lyb_mem_no_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_LYB, 0, LYD_PRINT_SHRINK, LYD_PARSE_STRICT | LYD_PARSE_ONLY, 0, ts_start, ts_end);
+}
+
+static LY_ERR
+test_parse_lyb_file_no_validate(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_parse(state, LYD_LYB, 1, 0, LYD_PARSE_STRICT | LYD_PARSE_ONLY, 0, ts_start, ts_end);
+}
+
+static LY_ERR
+_test_print(struct test_state *state, LYD_FORMAT format, uint32_t print_options, struct timespec *ts_start,
+        struct timespec *ts_end)
+{
+    LY_ERR ret = LY_SUCCESS;
+    char *buf = NULL;
+
+    TEST_START(ts_start);
+
+    if ((ret = lyd_print_mem(&buf, state->data1, format, print_options))) {
+        goto cleanup;
+    }
+
+    TEST_END(ts_end);
+
+cleanup:
+    free(buf);
+    return ret;
+}
+
+static LY_ERR
+test_print_xml(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_print(state, LYD_XML, LYD_PRINT_SHRINK, ts_start, ts_end);
+}
+
+static LY_ERR
+test_print_json(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_print(state, LYD_JSON, LYD_PRINT_SHRINK, ts_start, ts_end);
+}
+
+static LY_ERR
+test_print_lyb(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    return _test_print(state, LYD_LYB, LYD_PRINT_SHRINK, ts_start, ts_end);
+}
+
+static LY_ERR
+test_dup(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data;
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_dup_siblings(state->data1, NULL, LYD_DUP_RECURSIVE, &data))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(data);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_free(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data;
+
+    if ((r = create_list_inst(state->mod, 0, state->count, &data))) {
+        return r;
+    }
+
+    TEST_START(ts_start);
+
+    lyd_free_siblings(data);
+
+    TEST_END(ts_end);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_xpath_find(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct ly_set *set;
+    char path[64];
+
+    sprintf(path, "/perf:cont/lst[k1=%" PRIu32 " and k2='str%" PRIu32 "']", state->count / 2, state->count / 2);
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_find_xpath(state->data1, path, &set))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    ly_set_free(set, NULL);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_xpath_find_hash(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct ly_set *set;
+    char path[64];
+
+    sprintf(path, "/perf:cont/lst[k1=%" PRIu32 "][k2='str%" PRIu32 "']", state->count / 2, state->count / 2);
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_find_xpath(state->data1, path, &set))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    ly_set_free(set, NULL);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_compare_same(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_compare_siblings(state->data1, state->data2, LYD_COMPARE_FULL_RECURSION))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_diff_same(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *diff;
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_diff_siblings(state->data1, state->data2, 0, &diff))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(diff);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_diff_no_same(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *diff;
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_diff_siblings(state->data1, state->data2, 0, &diff))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(diff);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_merge_same(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_merge_siblings(&state->data1, state->data2, 0))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_merge_no_same(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data1;
+
+    if ((r = create_list_inst(state->mod, 0, state->count, &data1))) {
+        return r;
+    }
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_merge_siblings(&data1, state->data2, 0))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(data1);
+
+    return LY_SUCCESS;
+}
+
+static LY_ERR
+test_merge_no_same_destruct(struct test_state *state, struct timespec *ts_start, struct timespec *ts_end)
+{
+    LY_ERR r;
+    struct lyd_node *data1, *data2;
+
+    if ((r = create_list_inst(state->mod, 0, state->count, &data1))) {
+        return r;
+    }
+    if ((r = create_list_inst(state->mod, state->count, state->count, &data2))) {
+        return r;
+    }
+
+    TEST_START(ts_start);
+
+    if ((r = lyd_merge_siblings(&data1, data2, LYD_MERGE_DESTRUCT))) {
+        return r;
+    }
+
+    TEST_END(ts_end);
+
+    lyd_free_siblings(data1);
+
+    return LY_SUCCESS;
+}
+
+struct test tests[] = {
+    { "create new text", setup_basic, test_create_new_text },
+    { "create new bin", setup_basic, test_create_new_bin },
+    { "create path", setup_basic, test_create_path },
+    { "parse xml mem validate", setup_data_single_tree, test_parse_xml_mem_validate },
+    { "parse xml mem no validate", setup_data_single_tree, test_parse_xml_mem_no_validate },
+    { "parse xml file no validate format", setup_data_single_tree, test_parse_xml_file_no_validate_format },
+    { "parse json mem validate", setup_data_single_tree, test_parse_json_mem_validate },
+    { "parse json mem no validate", setup_data_single_tree, test_parse_json_mem_no_validate },
+    { "parse json file no validate format", setup_data_single_tree, test_parse_json_file_no_validate_format },
+    { "parse lyb mem validate", setup_data_single_tree, test_parse_lyb_mem_validate },
+    { "parse lyb mem no validate", setup_data_single_tree, test_parse_lyb_mem_no_validate },
+    { "parse lyb file no validate", setup_data_single_tree, test_parse_lyb_file_no_validate },
+    { "print xml", setup_data_single_tree, test_print_xml },
+    { "print json", setup_data_single_tree, test_print_json },
+    { "print lyb", setup_data_single_tree, test_print_lyb },
+    { "dup", setup_data_single_tree, test_dup },
+    { "free", setup_basic, test_free },
+    { "xpath find", setup_data_single_tree, test_xpath_find },
+    { "xpath find hash", setup_data_single_tree, test_xpath_find_hash },
+    { "compare same", setup_data_same_trees, test_compare_same },
+    { "diff same", setup_data_same_trees, test_diff_same },
+    { "diff no same", setup_data_no_same_trees, test_diff_no_same },
+    { "merge same", setup_data_same_trees, test_merge_same },
+    { "merge no same", setup_data_offset_tree, test_merge_no_same },
+    { "merge no same destruct", setup_basic, test_merge_no_same_destruct },
+};
+
+int
+main(int argc, char **argv)
+{
+    LY_ERR ret = LY_SUCCESS;
+    struct ly_ctx *ctx = NULL;
+    const struct lys_module *mod;
+    uint32_t i, count, tries;
+
+    if (argc < 3) {
+        fprintf(stderr, "Usage:\n%s list-instance-count test-tries\n\n", argv[0]);
+        return LY_EINVAL;
+    }
+
+    count = atoi(argv[1]);
+    if (!count) {
+        fprintf(stderr, "Invalid count \"%s\".\n", argv[1]);
+        return LY_EINVAL;
+    }
+
+    tries = atoi(argv[2]);
+    if (!tries) {
+        fprintf(stderr, "Invalid tries \"%s\".\n", argv[2]);
+        return LY_EINVAL;
+    }
+
+    printf("\nly_perf:\n\tdata set size: %" PRIu32 "\n\teach test executed: %" PRIu32 " %s\n\n", count, tries,
+            (tries > 1) ? "times" : "time");
+
+    /* create context */
+    if ((ret = ly_ctx_new(TESTS_SRC "/perf", 0, &ctx))) {
+        goto cleanup;
+    }
+
+    /* load modules */
+    if (!(mod = ly_ctx_load_module(ctx, "perf", NULL, NULL))) {
+        ret = LY_ENOTFOUND;
+        goto cleanup;
+    }
+
+    /* tests */
+    for (i = 0; i < (sizeof tests / sizeof(struct test)); ++i) {
+        if ((ret = exec_test(tests[i].setup, tests[i].test, tests[i].name, mod, count, tries))) {
+            goto cleanup;
+        }
+    }
+
+    printf("\n");
+
+cleanup:
+    ly_ctx_destroy(ctx);
+    return ret;
+}
diff --git a/tests/perf/perf.yang b/tests/perf/perf.yang
new file mode 100644
index 0000000..9614945
--- /dev/null
+++ b/tests/perf/perf.yang
@@ -0,0 +1,23 @@
+module perf {
+    yang-version 1.1;
+    namespace "urn:sysrepo:tests:perf";
+    prefix p;
+
+    container cont {
+        list lst {
+            key "k1 k2";
+
+            leaf k1 {
+                type uint32;
+            }
+
+            leaf k2 {
+                type string;
+            }
+
+            leaf l {
+                type string;
+            }
+        }
+    }
+}
diff --git a/tests/tests_config.h.in b/tests/tests_config.h.in
index 125b856..119fb35 100644
--- a/tests/tests_config.h.in
+++ b/tests/tests_config.h.in
@@ -19,4 +19,9 @@
 
 #define TESTS_DIR_MODULES_YANG TESTS_SRC"/modules/yang"
 
+/**
+ * @brief Macro for support of callgrind header and macros.
+ */
+#cmakedefine HAVE_CALLGRIND
+
 #endif /* LYTEST_CONFIG_H_ */