diff --git a/.gitignore b/.gitignore index ebb181e..be47746 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ dto-test* libdto.so* +libdto_explicit.so* build*/ tests/baselines*/ +dtoctl +/doc/dtoctl.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e8cccd..2c83353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,20 @@ cmake_minimum_required(VERSION 3.5...3.31) project(DTO VERSION 1.0 LANGUAGES C) -# Default to RelWithDebInfo when no build type is specified. +# DTO may be built on its own or as part of another project (add_subdirectory +# or FetchContent). As a subproject only the library is built by default, and +# the parent's settings are left alone. +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(DTO_IS_TOP_LEVEL ON) +else() + set(DTO_IS_TOP_LEVEL OFF) +endif() + +# Default to RelWithDebInfo when no build type is specified. Only at the top +# level: CMAKE_BUILD_TYPE is the parent's when DTO is a subproject. # (Skip for multi-config generators, which ignore CMAKE_BUILD_TYPE.) get_property(_is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(NOT _is_multi_config AND NOT CMAKE_BUILD_TYPE) +if(DTO_IS_TOP_LEVEL AND NOT _is_multi_config AND NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Choose the type of build" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS @@ -55,7 +65,10 @@ target_compile_definitions(dto PRIVATE DTO_STATS_SUPPORT) # Link libraries set(DTO_STATIC_LIBS "") -set(DTO_DYNAMIC_LIBS dl) +# pthread: stats use thread keys and a sampler thread; before glibc 2.34 these +# are not in libc, so a program that doesn't link libpthread itself would fail +# to load libdto. +set(DTO_DYNAMIC_LIBS dl pthread) # Handle accel-config support if(DTO_ACCEL_CONFIG_SUPPORT) @@ -124,15 +137,65 @@ if (HAS_WAITPKG) endforeach() endif() -# Build dto-test and dto-test-wodto -add_executable(dto-test dto-test.c) -target_link_libraries(dto-test PRIVATE DTO::dto pthread) +# Build dtoctl. By default it preloads the libdto.so installed alongside it, +# found relative to the dtoctl binary so "cmake --install --prefix" still works, +# and falls back to the configured install path. Set DTOCTL_LIBPATH to always +# preload a specific library instead. +set(DTOCTL_LIBPATH "" CACHE FILEPATH + "Path to libdto.so that dtoctl preloads by default (empty: the installed libdto.so)") +add_executable(dtoctl dtoctl.c) +target_compile_definitions(dtoctl PRIVATE DTO_VERSION="${PROJECT_VERSION}") +if(DTOCTL_LIBPATH) + target_compile_definitions(dtoctl PRIVATE LIBDTO_PATH="${DTOCTL_LIBPATH}") +else() + # Not cached, so it follows later changes to CMAKE_INSTALL_PREFIX. + get_filename_component(DTOCTL_LIBDIR "${CMAKE_INSTALL_LIBDIR}" ABSOLUTE + BASE_DIR "${CMAKE_INSTALL_PREFIX}") + get_filename_component(DTOCTL_BINDIR "${CMAKE_INSTALL_BINDIR}" ABSOLUTE + BASE_DIR "${CMAKE_INSTALL_PREFIX}") + file(RELATIVE_PATH DTOCTL_REL_LIBPATH "${DTOCTL_BINDIR}" "${DTOCTL_LIBDIR}/libdto.so") + target_compile_definitions(dtoctl PRIVATE + LIBDTO_PATH="${DTOCTL_LIBDIR}/libdto.so" + LIBDTO_REL_PATH="${DTOCTL_REL_LIBPATH}") +endif() +install(TARGETS dtoctl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# dto-stats-report summarizes "dtoctl --stats-run"; dtoctl looks for it next to +# itself first, so copy it beside dtoctl in the build tree too. +configure_file(dto-stats-report ${CMAKE_CURRENT_BINARY_DIR}/dto-stats-report COPYONLY) +install(PROGRAMS dto-stats-report DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# Build and install the optional section 1 manual. +find_program(PANDOC_EXECUTABLE NAMES pandoc) +if(PANDOC_EXECUTABLE) + set(DTOCTL_MAN_PAGE "${CMAKE_CURRENT_BINARY_DIR}/doc/dtoctl.1") + add_custom_command( + OUTPUT "${DTOCTL_MAN_PAGE}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/doc" + COMMAND "${PANDOC_EXECUTABLE}" --standalone --to=man + "${CMAKE_CURRENT_SOURCE_DIR}/doc/dtoctl.md" -o "${DTOCTL_MAN_PAGE}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/doc/dtoctl.md" + COMMENT "Generating dtoctl(1) manual" + VERBATIM) + add_custom_target(dtoctl-man ALL DEPENDS "${DTOCTL_MAN_PAGE}") + install(FILES "${DTOCTL_MAN_PAGE}" DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) +else() + message(STATUS "Skipping dtoctl manual generation and installation: install pandoc to build the man page.") +endif() -add_executable(dto-test-wodto dto-test.c) -target_link_libraries(dto-test-wodto PRIVATE pthread) +# Build dto-test and dto-test-wodto +option(DTO_BUILD_TEST_APPS "Build the dto-test and dto-test-wodto applications" + ${DTO_IS_TOP_LEVEL}) +if(DTO_BUILD_TEST_APPS) + add_executable(dto-test dto-test.c) + target_link_libraries(dto-test PRIVATE DTO::dto pthread) + + add_executable(dto-test-wodto dto-test.c) + target_link_libraries(dto-test-wodto PRIVATE pthread) +endif() # ---- Test Suite ---- -option(DTO_BUILD_TESTS "Build the DTO test suite" ON) +option(DTO_BUILD_TESTS "Build the DTO test suite" ${DTO_IS_TOP_LEVEL}) if(DTO_BUILD_TESTS) enable_testing() @@ -158,6 +221,41 @@ if(DTO_BUILD_TESTS) TIMEOUT 120 ) endif() + # dtoctl option handling and environment setup (no DSA hardware needed) + add_test(NAME dtoctl + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_dtoctl.sh + $ $) + set_tests_properties(dtoctl PROPERTIES + LABELS "functional" + TIMEOUT 60 + ) + # Stats collection (DTO_COLLECT_STATS): a workload of known calls, checked + # by a Python script. Runs without DSA (everything is counted as CPU). + find_package(Python3 COMPONENTS Interpreter QUIET) + add_executable(dto-test-stats-workload tests/test_stats_workload.c) + target_compile_options(dto-test-stats-workload PRIVATE -fno-builtin) + target_link_libraries(dto-test-stats-workload PRIVATE DTO::dto pthread) + if(Python3_Interpreter_FOUND) + add_test(NAME stats + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_stats.py + $ $) + set_tests_properties(stats PROPERTIES + LABELS "functional" + TIMEOUT 120 + ) + else() + message(STATUS "stats test: disabled (python3 not found)") + endif() + # dto-stats-report's arithmetic, on made-up runs + if(Python3_Interpreter_FOUND) + add_test(NAME stats-report + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_stats_report.py + ${CMAKE_CURRENT_BINARY_DIR}/dto-stats-report) + set_tests_properties(stats-report PROPERTIES + LABELS "functional" + TIMEOUT 60 + ) + endif() # ---- A/B performance regression test ---- # The baseline object is compiled from unguarded upstream sources that @@ -176,20 +274,20 @@ if(DTO_BUILD_TESTS) endif() # Build current dto.c as object file with renamed symbols (cur_memcpy, etc.) - set(PERF_BASELINE_DIR "${CMAKE_BINARY_DIR}/perf_baseline") + set(PERF_BASELINE_DIR "${CMAKE_CURRENT_BINARY_DIR}/perf_baseline") add_custom_command( - OUTPUT ${CMAKE_BINARY_DIR}/dto_current.o + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/dto_current.o COMMAND ${CMAKE_C_COMPILER} -c ${PERF_OBJ_CFLAGS} - ${CMAKE_SOURCE_DIR}/dto.c - -o ${CMAKE_BINARY_DIR}/dto_current_raw.o + ${CMAKE_CURRENT_SOURCE_DIR}/dto.c + -o ${CMAKE_CURRENT_BINARY_DIR}/dto_current_raw.o COMMAND objcopy --redefine-sym memcpy=cur_memcpy --redefine-sym memset=cur_memset --redefine-sym memcmp=cur_memcmp --redefine-sym memmove=cur_memmove - ${CMAKE_BINARY_DIR}/dto_current_raw.o - ${CMAKE_BINARY_DIR}/dto_current.o - DEPENDS ${CMAKE_SOURCE_DIR}/dto.c + ${CMAKE_CURRENT_BINARY_DIR}/dto_current_raw.o + ${CMAKE_CURRENT_BINARY_DIR}/dto_current.o + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/dto.c COMMENT "Building dto_current.o with renamed symbols" ) @@ -200,8 +298,8 @@ if(DTO_BUILD_TESTS) # object is still fresh, and a custom command would never re-run once # the object exists. add_custom_target(dto_baseline_build - COMMAND bash ${CMAKE_SOURCE_DIR}/tests/build_baseline.sh - ${CMAKE_SOURCE_DIR} ${PERF_BASELINE_DIR} ${CMAKE_C_COMPILER} + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/tests/build_baseline.sh + ${CMAKE_CURRENT_SOURCE_DIR} ${PERF_BASELINE_DIR} ${CMAKE_C_COMPILER} ${PERF_OBJ_CFLAGS} BYPRODUCTS ${PERF_BASELINE_DIR}/dto_baseline.o COMMENT "Building dto_baseline.o from baseline branch" @@ -209,7 +307,7 @@ if(DTO_BUILD_TESTS) add_executable(dto-test-perf tests/test_perf.c - ${CMAKE_BINARY_DIR}/dto_current.o + ${CMAKE_CURRENT_BINARY_DIR}/dto_current.o ${PERF_BASELINE_DIR}/dto_baseline.o ) add_dependencies(dto-test-perf dto_baseline_build) @@ -241,7 +339,7 @@ if(DTO_BUILD_TESTS) LABELS "perf" TIMEOUT 2400 RUN_SERIAL TRUE - ENVIRONMENT "RESULTS_DIR=${CMAKE_BINARY_DIR}/perf_results;${PERF_COMMON_ENV}" + ENVIRONMENT "RESULTS_DIR=${CMAKE_CURRENT_BINARY_DIR}/perf_results;${PERF_COMMON_ENV}" ) # short sanity variant: memcpy 4k/64k/1m + memset 64k, @@ -252,7 +350,7 @@ if(DTO_BUILD_TESTS) LABELS "perf" TIMEOUT 120 RUN_SERIAL TRUE - ENVIRONMENT "PERF_SHORT=1;RESULTS_DIR=${CMAKE_BINARY_DIR}/perf_results_sanity;${PERF_COMMON_ENV}" + ENVIRONMENT "PERF_SHORT=1;RESULTS_DIR=${CMAKE_CURRENT_BINARY_DIR}/perf_results_sanity;${PERF_COMMON_ENV}" ) else() message(STATUS "perf test: disabled (requires DTO_ACCEL_CONFIG_SUPPORT and " diff --git a/Makefile b/Makefile index 4da8111..15467f5 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,21 @@ # # SPDX-License-Identifier: MIT -all: libdto dto-test-wodto +all: libdto dtoctl dto-test-wodto man + +.PHONY: man install-man DML_LIB_CXX=-D_GNU_SOURCE +# Path to libdto.so that dtoctl prepends to LD_PRELOAD (matches "make install"). +DTOCTL_LIBPATH ?= /usr/lib64/libdto.so +PANDOC ?= pandoc +MANDIR ?= /usr/share/man +# Version reported by "dtoctl --version", taken from the CMake project. +DTO_VERSION := $(shell grep -o 'DTO VERSION [0-9.]*' CMakeLists.txt | cut -d' ' -f3) + libdto: dto.c - gcc -shared -fPIC -Wl,-soname,libdto.so dto.c $(DML_LIB_CXX) -DDTO_STATS_SUPPORT -DDTO_ACCEL_CONFIG_SUPPORT -DDTO_NUMA_SUPPORT -o libdto.so.1.0 -laccel-config -ldl -lnuma -mwaitpkg + gcc -shared -fPIC -Wl,-soname,libdto.so dto.c $(DML_LIB_CXX) -DDTO_STATS_SUPPORT -DDTO_ACCEL_CONFIG_SUPPORT -DDTO_NUMA_SUPPORT -o libdto.so.1.0 -laccel-config -ldl -lnuma -lpthread -mwaitpkg libdto_nostats: dto.c gcc -shared -fPIC -Wl,-soname,libdto.so dto.c $(DML_LIB_CXX) -DDTO_ACCEL_CONFIG_SUPPORT -DDTO_NUMA_SUPPORT -o libdto.so.1.0 -laccel-config -ldl -lnuma -mwaitpkg @@ -16,11 +25,32 @@ libdto_nostats: dto.c libdto_explicit: dto.c gcc -shared -fPIC -Wl,-soname,libdto_explicit.so dto.c $(DML_LIB_CXX) -DDTO_NO_LIBC_INTERPOSE -DDTO_ACCEL_CONFIG_SUPPORT -DDTO_NUMA_SUPPORT -o libdto_explicit.so.1.0 -laccel-config -ldl -lnuma -mwaitpkg -install: +dtoctl: dtoctl.c + gcc -O2 -Wall dtoctl.c -DLIBDTO_PATH=\"$(DTOCTL_LIBPATH)\" -DDTO_VERSION=\"$(DTO_VERSION)\" -o dtoctl + +man: doc/dtoctl.1 + +doc/dtoctl.1: doc/dtoctl.md + @if command -v "$(PANDOC)" >/dev/null 2>&1; then \ + "$(PANDOC)" --standalone --to=man $< -o $@; \ + else \ + echo "Skipping dtoctl manual generation: install pandoc to build the man page."; \ + fi + +install: dtoctl install-man cp libdto.so.1.0 /usr/lib64/ cp dto.h /usr/include/ ln -sf /usr/lib64/libdto.so.1.0 /usr/lib64/libdto.so.1 ln -sf /usr/lib64/libdto.so.1.0 /usr/lib64/libdto.so + cp dtoctl /usr/bin/ + cp dto-stats-report /usr/bin/ + +install-man: man + @if [ -f doc/dtoctl.1 ]; then \ + install -D -m 644 doc/dtoctl.1 "$(MANDIR)/man1/dtoctl.1"; \ + else \ + echo "Skipping dtoctl manual installation: no generated man page is available."; \ + fi install-local: ln -sf ./libdto.so.1.0 ./libdto.so.1 @@ -33,4 +63,5 @@ dto-test-wodto: dto-test.c gcc -g dto-test.c $(DML_LIB_CXX) -o dto-test-wodto -lpthread clean: - rm -rf *.o *.so dto-test + rm -rf *.o *.so dto-test dtoctl + rm -f doc/dtoctl.1 diff --git a/README.md b/README.md index 42cda0e..bed25a0 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,14 @@ dto-4-dsa.conf: An example json config file for configuring DSAs Following environment variables control the behavior of DTO library: DTO_USESTDC_CALLS=0/1, 1 (uses std c memory functions only), 0 (uses DSA along with std c lib call; in case of DSA page fault - reverts to std c lib call). Default is 0. DTO_COLLECT_STATS=0/1, 1 (enables stats collection - #of operations, avg latency for each API, etc.>, 0 (disables stats collection). - Should be enabled for debugging/profiling only, not for perf evaluation (enabling it slows down the workload). Default is 0. - DTO_STATS_FILE= when DTO_COLLECT_STATS=1, redirect histogram output to the provided regular file instead of standard output. - The file is truncated when DTO loads; failures fall back to standard output and emit an error. - DTO_WAIT_METHOD= (specifies the method to use while waiting for DSA to complete operation, default is yield) - DTO_MIN_BYTES=xxxx (specifies minimum size of API call needed for DSA operation execution, default is 16384 bytes) + Adds about 40 ns per call (two clock reads), whatever the number of threads. Default is 0. See Statistics below. + DTO_STATS_FILE= when DTO_COLLECT_STATS=1, write stats to the provided regular file instead of standard output. Each %p in + the path is replaced by the process ID. Records are appended, not truncated: processes started from this one + append to the same file unless the path has a %p. Failures fall back to standard output and emit an error. + DTO_STATS_FORMAT=text/json text (default): the tables shown below; json: JSON Lines, one record per line (see Statistics). + DTO_STATS_INTERVAL_MS=N when N > 0, also write what happened in every N ms, from a background thread. Default is 0. + DTO_WAIT_METHOD= (specifies the method to use while waiting for DSA to complete operation, default is busypoll) + DTO_MIN_BYTES=xxxx (specifies minimum size of API call needed for DSA operation execution, default is 65536 bytes) DTO_CPU_SIZE_FRACTION=0.xx (specifies fraction of job performed by CPU, in parallel to DSA). Default is 0.00 DTO_AUTO_ADJUST_KNOBS=0/1 (disables/enables auto tuning of cpu_size_fraction and dsa_min_bytes parameters. 0 -- disable, 1 -- enable (default)) DTO_IS_NUMA_AWARE=0/1/2 (disables/buffer-centric/cpu-centric numa awareness. 0 -- disable (default), 1 -- buffer-centric, 2 - cpu-centric) @@ -69,7 +72,7 @@ Following environment variables control the behavior of DTO library: DTO_DSA_CC=0/1, 1 (default) - DTO sets DSA Cache Control flag to 1 if DSA supports cache control, 0 - DTO sets DSA Cache Control flag to 0 DTO_OVERLAPPING_MEMMOVE_ACTION=0/1 0 (default) DTO submits memmove operations with overlapping buffers entirely to CPU, 1 - entirely to DSA DTO_UMWAIT_DELAY=xxxx defines delay for umwait command (check max possible value at: /sys/devices/system/cpu/umwait_control/max_time), default is 100000 - DTO_LOG_FILE= Redirect the DTO output to the specified file instead of std output (useful for debugging and statistics collection). file name is suffixed by process pid. + DTO_LOG_FILE= Redirect the DTO output to the specified file instead of std output (useful for debugging and statistics collection). file name is suffixed by the program name and process pid (..). DTO_LOG_LEVEL=0/1/2 controls the log level. higher value means more verbose logging (default 0). DTO_CRC_MIN_BYTES=xxxx smallest buffer dto_crc32c_with_seed() sends to DSA; smaller ones are checksummed on the CPU (default: DTO_MIN_BYTES). ``` @@ -140,6 +143,144 @@ Programs that only use this API, and must not have their own large libc copies sent to DSA, can link `libdto_explicit` instead: the same library without the memcpy/memset/memmove/memcmp interposers (`-ldto_explicit`, or `DTO::dto_explicit` from CMake). +## Using dtoctl + +Setting `LD_PRELOAD` and exporting the `DTO_*` environment variables by hand is +error prone. The `dtoctl` tool provides a simpler interface: it takes the DTO +settings as command line options, converts them to the corresponding `DTO_*` +environment variables, prepends `libdto.so` to `LD_PRELOAD`, and then runs the +target program. For example, instead of + +```bash +export LD_PRELOAD=/usr/local/lib/libdto.so +export DTO_WAIT_METHOD=busypoll +export DTO_CPU_SIZE_FRACTION=0.33 +export DTO_AUTO_ADJUST_KNOBS=1 +./prog +``` + +you can run + +```bash +dtoctl -w busypoll -c 0.33 ./prog +``` + +Every environment variable listed above has a corresponding option, and option +values are checked before the program runs (DTO itself silently falls back to a +default on a value it does not understand). Only the options you pass are +applied: a `DTO_*` variable you already exported is left untouched, an explicit +option overrides it, and `-i/--ignore-env` drops all inherited `DTO_*` +variables. + +The `libdto.so` path is resolved from `-l/--library`, then `$DTO_LIBRARY`, then +the library installed with `dtoctl`, then the compiled-in default +(`/usr/local/lib/libdto.so` for a CMake build, `/usr/lib64/libdto.so` for the +Makefile build). It is made absolute and prepended to any existing +`LD_PRELOAD`, and `dtoctl` fails if the library cannot be read rather than +running the program without DTO. + +To see what would run without running it, use `--dry-run`: + +```bash +$ dtoctl --dry-run -w umwait -b 16K ./prog +library: /usr/lib64/libdto.so.1.0 (compiled-in default) +LD_PRELOAD=/usr/lib64/libdto.so.1.0 +DTO_WAIT_METHOD=umwait # option +DTO_MIN_BYTES=16384 # option +command: ./prog +``` + +DSA work queues are often accessible to root only. Run `sudo dtoctl ...` +rather than `dtoctl sudo ...`, which loses `LD_PRELOAD` and the `DTO_*` +variables, and pass settings as options: `sudo` does not pass on variables +exported in your shell. + +### Measuring the speedup from DSA + +`dtoctl --stats-run` runs a program CPU-only (`DTO_USESTDC_CALLS=1`) and then +with DSA, collecting DTO's JSON stats (see [Statistics](#statistics)) from each +run into a directory, and `dto-stats-report` (installed with `dtoctl`) compares +them. With `--repeat=N` each way runs N times, alternating, and the report uses +the median wall time and, for the CPU and mem* times, the run with the median +CPU time; `--perf` adds one more run each way under `perf record`. + +```bash +sudo taskset -c 2 dtoctl -w busypoll --no-auto-adjust -c 0 --stats-run=app-stats --repeat=3 --perf ./app +``` + +``` +DTO stats run: app-stats + command: ./app + settings: DTO_AUTO_ADJUST_KNOBS=0 DTO_CPU_SIZE_FRACTION=0 DTO_WAIT_METHOD=busypoll + timed runs: 3 CPU-only, 3 with DSA (medians) + DTO in the DSA run: 1 WQs, wait busypoll, min_bytes 65536, cpu_size_fraction 0.0 + + CPU only with DSA speedup + wall time 1.66 s 1.38 s 1.20x + CPU time (user+sys) 1.65 s 1.38 s 1.20x + in mem* calls 782.69 ms 493.11 ms 1.59x + outside mem* calls 868.09 ms 885.71 ms 0.98x + wall time, min-max 1.66-1.67 s 1.38-1.40 s + + Amdahl's law + f, share of CPU time in mem* calls (CPU-only) 47.4% + s, mem* speedup with DSA (time per byte) 1.59x + predicted speedup, 1 / ((1 - f) + f / s) 1.21x + measured speedup, wall time 1.20x + limit if mem* took no time, 1 / (1 - f) 1.90x + + per operation calls bytes avg size CPU only with DSA speedup on DSA f + memcpy 20.00k 4.9 GiB 256.0 KiB 596.44 ms 180.24 ms 3.31x 99.4% 36.1% + memset 20.00k 4.9 GiB 256.0 KiB 92.17 ms 100.59 ms 0.92x 100.0% 5.6% + memcmp 20.00k 4.9 GiB 256.0 KiB 94.09 ms 212.28 ms 0.44x 100.0% 5.7% + (on DSA: share of the bytes in calls sent to DSA, of which the CPU does cpu_size_fraction; + f: share of the CPU-only run's CPU time) + calls DSA left to the CPU, per run: page fault 128, retry 0, other 0 + + by size (most time) calls CPU only with DSA speedup on DSA + 256K-320K 60.00k 13.04 us 8.22 us 1.59x 99.8% + (average time per call; on DSA: share of calls sent to DSA) + + profile (perf, % of CPU time) CPU only with DSA + memcpy/memmove 35.0% 0.3% + memset 6.0% 0.0% + memcmp 5.3% 0.0% + CRC 9.8% 12.5% + DTO library 0.3% 28.7% + other user code 31.2% 43.8% + other kernel code 12.4% 14.8% + + note: 10% of the CPU-only run is CRC code: DSA can compute CRC32C, but DTO doesn't intercept CRC functions, so offloading it needs an explicit call +``` + +Here a single thread, on a core of the DSA's NUMA node, copies 256 KiB at a +time out of a 1 GiB buffer, so the data is not in the CPU caches, compares the +copy with the source, clears it, computes a CRC32C in software, and does other +work. The CPU-only run spent f = 47.4% of its CPU time in mem\* calls, and +those calls took s = 1.59x less time per byte with DSA, so Amdahl's law +predicts 1 / ((1 - f) + f / s) = 1.21x for the whole program; it measured +1.20x. At best, mem\* calls taking no time at all, it could run 1.90x faster. +The copies were 3.31x faster with DSA, but the compares, which read the copy +right after it is made, were slower: a copy on the CPU leaves the data in its +caches and a copy by DSA does not. The auto-tuner is off in this run +(`--no-auto-adjust -c 0`); with it on, it gave part of each copy to the CPU and +the program ran only 1.02x faster. + +The report also shows each operation and the sizes with the most time, so you +can see where DSA wins and set `DTO_MIN_BYTES` accordingly, and notes anything +that makes the comparison doubtful. Among them: no call reaching DSA (no usable +work queue: run with `sudo`), and runs that did different amounts of work, as +a benchmark that runs for a fixed time does (speedups are then per byte). The +perf profile splits each run's CPU time into copies, memset, memcmp, CRC, +kernel copies, DTO itself (here, busy-polling for completions) and the rest. It +also counts mem\* calls DTO cannot intercept, such as those made inside libc. +Run `dto-stats-report app-stats` again at any time, or with `--json` for the +numbers. Amdahl's estimate assumes the program keeps its CPUs busy; one that +waits for I/O gains less in wall time. + +Run `dtoctl --help` for the option list, or `man 1 dtoctl` for the full manual, +which documents each option and gives a ready-to-run command for each of the +usage models above. ## Build @@ -171,6 +312,8 @@ The accel-config and libnuma dependencies are optional and can be disabled at co | `DTO_STATIC_ACCEL_CONFIG` | `OFF` | Statically link libaccel-config into libdto.so instead of depending on the shared library at runtime. Requires `libaccel-config.a` to be installed. | | `DTO_STATIC_NUMA` | `OFF` | Statically link libnuma into libdto.so instead of depending on the shared library at runtime. Requires a `libnuma.a` compiled with `-fPIC`. | | `DTO_BUILD_EXPLICIT` | `ON` | Also build `libdto_explicit`, the explicit API (`dto.h`) without the libc interposers. | +| `DTO_BUILD_TESTS` | `ON` (`OFF` as a subproject) | Build the test suite (see [TESTING.md](TESTING.md)). | +| `DTO_BUILD_TEST_APPS` | `ON` (`OFF` as a subproject) | Build the `dto-test` and `dto-test-wodto` applications. | Example: build without either dependency: ```bash @@ -185,6 +328,17 @@ you need to build without them. The CMake build also produces the test applications in the build directory: `dto-test` (for the -ldto method) and `dto-test-wodto` (for the LD_PRELOAD method). +DTO can also be built as part of another CMake project, with `add_subdirectory` +or `FetchContent`, and linked as `DTO::dto`. Only the library is built then, +unless `DTO_BUILD_TESTS` or `DTO_BUILD_TEST_APPS` is set, and the parent +project's build type is left as it is: +```cmake +include(FetchContent) +FetchContent_Declare(dto GIT_REPOSITORY https://github.com/intel/DTO.git GIT_TAG main) +FetchContent_MakeAvailable(dto) +target_link_libraries(myapp PRIVATE DTO::dto) +``` + CMake installs the library to `/usr/local/lib` by default, while the deprecated Makefile installs to `/usr/lib64`. Pass `-DCMAKE_INSTALL_PREFIX=` at configure time to change the install location. @@ -242,63 +396,76 @@ to the DSA device. /cachebench -json_test_config --progress_stats_file=dto.log --report_api_latency -4. Sample histogram given below (generated using DTO_COLLECT_STATS=1) +4. Sample stats output (DTO_COLLECT_STATS=1; see [Statistics](#statistics)) i. Numbers under columns set, cpy, mov, and cmp show number of API calls or per-API completion latency for memset, memcpy, memmove, and memcmp respectively. ii. Numbers in bytes column show total bytes processed across all 4 API calls ------------------------------------------------------------------------------------------------------------------------------------------ -DTO Run Time: 22826 ms +``` +DTO Run Time: 8 ms ******** Number of Memory Operations ******** - <*************** stdc calls ***************> <*************** dsa (success) ***************> <*************** dsa (failed) ***************> <***** failure reason *****> -Byte Range -- set cpy mov cmp bytes set cpy mov cmp bytes set cpy mov cmp bytes Retries PFs Others - 0-4095 -- 24616626 12319310 7676731 244777209 4531255821 0 0 0 0 0 0 0 0 0 0 0 0 0 - 4096-8191 -- 3 290 14 0 1842507 0 0 0 0 0 0 0 0 0 0 0 0 0 - 8192-12287 -- 0 2 6 0 45579 64 176 2 0 2545968 0 2 6 0 25276 0 8 0 - 12288-16383 -- 1 1 0 0 20756 1 137 0 0 1965343 1 1 0 0 11172 0 2 0 - 16384-20479 -- 0 1 3 0 41974 0 126 2 0 2328434 0 1 3 0 24337 0 4 0 - 32768-36863 -- 0 0 0 0 0 0 67 2 0 2378165 0 0 0 0 0 0 0 0 - 53248-57343 -- 0 0 0 0 0 0 46 0 0 2545890 0 0 0 0 0 0 0 0 - 98304-102399 -- 0 0 0 0 0 0 13 0 0 1309627 0 0 0 0 0 0 0 0 - 106496-110591 -- 0 1 0 0 31637 0 8 0 0 869880 0 1 0 0 77586 0 1 0 - 118784-122879 -- 0 0 0 0 0 0 9 0 0 1083519 0 0 0 0 0 0 0 0 - 131072-135167 -- 0 0 2 0 175638 0 5 0 0 665475 0 0 2 0 86506 0 2 0 - 143360-147455 -- 0 0 0 0 0 0 1 0 0 143399 0 0 0 0 0 0 0 0 - 147456-151551 -- 0 0 0 0 0 0 1 0 0 148647 0 0 0 0 0 0 0 0 - 159744-163839 -- 0 0 0 0 0 0 6 0 0 970186 0 0 0 0 0 0 0 0 - 212992-217087 -- 0 0 11 0 1580248 0 4 2 0 1292252 0 0 11 0 779384 0 11 0 - 278528-282623 -- 0 0 0 0 0 0 1 0 0 279719 0 0 0 0 0 0 0 0 - 466944-471039 -- 0 0 0 0 0 0 1 0 0 467783 0 0 0 0 0 0 0 0 - 528384-532479 -- 0 0 0 0 0 0 1 0 0 529927 0 0 0 0 0 0 0 0 - 720896-724991 -- 0 0 0 0 0 1 0 0 0 720896 0 0 0 0 0 0 0 0 - 999424-1003519 -- 0 0 0 0 0 0 1 0 0 1002439 0 0 0 0 0 0 0 0 - >=2093056 -- 0 1 0 0 1975911 0 0 0 0 0 0 1 0 0 973209 0 1 0 + <*************** stdc calls ***************> <*************** dsa (success) ***************> <*************** dsa (failed) ***************> <***** failure reason *****> +Byte Range -- set cpy mov cmp bytes set cpy mov cmp bytes set cpy mov cmp bytes Retry PFs Others + 65536-81919 -- 0 0 0 0 0 200 0 0 0 13926400 0 0 0 0 0 0 0 0 + 98304-114687 -- 0 0 200 0 19662200 0 0 0 0 0 0 0 0 0 0 0 0 0 + 131072-163839 -- 0 0 0 0 0 0 200 0 0 26239000 0 0 0 0 0 0 0 0 + 262144-327679 -- 0 0 0 0 0 0 0 0 200 52441600 0 0 0 0 0 0 0 0 ******** Average Memory Operation Latency (us) ******** - <******** stdc calls ********> <******** dsa (success) ********> <******** dsa (failed) ********> -Byte Range -- set cpy mov cmp set cpy mov cmp set cpy mov cmp - 0-4095 -- 0.01 0.02 0.01 0.04 0 0 0 0 0 0 0 0 - 4096-8191 -- 0.07 0.42 0.47 0 0 0 0 0 0 0 0 0 - 8192-12287 -- 0 1.29 0.47 0 0.84 1.19 1.82 0 0 56.18 2.54 0 - 12288-16383 -- 2.90 3.34 0 0 7.49 1.26 0 0 2.07 1.93 0 0 - 16384-20479 -- 0 0.27 3.24 0 0 1.37 1.86 0 0 84.92 2.47 0 - 32768-36863 -- 0 0 0 0 0 1.87 2.91 0 0 0 0 0 - 53248-57343 -- 0 0 0 0 0 2.45 0 0 0 0 0 0 - 98304-102399 -- 0 0 0 0 0 3.76 0 0 0 0 0 0 - 106496-110591 -- 0 229.18 0 0 0 4.19 0 0 0 3.65 0 0 - 118784-122879 -- 0 0 0 0 0 5.18 0 0 0 0 0 0 - 131072-135167 -- 0 0 25.00 0 0 5.92 0 0 0 0 14.39 0 - 143360-147455 -- 0 0 0 0 0 5.11 0 0 0 0 0 0 - 147456-151551 -- 0 0 0 0 0 6.16 0 0 0 0 0 0 - 159744-163839 -- 0 0 0 0 0 7.65 0 0 0 0 0 0 - 212992-217087 -- 0 0 36.10 0 0 8.15 6.59 0 0 0 18.07 0 - 278528-282623 -- 0 0 0 0 0 15.47 0 0 0 0 0 0 - 466944-471039 -- 0 0 0 0 0 15.91 0 0 0 0 0 0 - 528384-532479 -- 0 0 0 0 0 18.65 0 0 0 0 0 0 - 720896-724991 -- 0 0 0 0 17.71 0 0 0 0 0 0 0 - 999424-1003519 -- 0 0 0 0 0 28.42 0 0 0 0 0 0 - >=2093056 -- 0 422.15 0 0 0 0 0 0 0 272.87 0 0 + <******** stdc calls ********> <******** dsa (success) ********> <******** dsa (failed) ********> +Byte Range -- set cpy mov cmp set cpy mov cmp set cpy mov cmp + 65536-81919 -- 0 0 0 0 8.06 0 0 0 0 0 0 0 + 98304-114687 -- 0 0 4.18 0 0 0 0 0 0 0 0 0 + 131072-163839 -- 0 0 0 0 0 17.02 0 0 0 0 0 0 + 262144-327679 -- 0 0 0 0 0 0 0 34.50 0 0 0 0 +``` + +## Statistics + +With `DTO_COLLECT_STATS=1`, DTO times and counts every memset, memcpy, +memmove and memcmp it intercepts, by operation, by path (CPU, DSA, or DSA +failed and finished on the CPU) and by size, and writes a summary when the +process exits (or on Ctrl-C). Each thread counts on its own, so collection +costs about 40 ns per call however many threads there are, and the counts +are summed for output. + +The size histogram has a bucket for calls under 64 bytes, then four buckets +per power of two up to 4 GiB (65536-81919, 81920-98303, ...), and one for +anything larger. A call that DSA completed only partly (for example on a +page fault) is counted once as `dsa (failed)` and once as a CPU call for the +remainder. + +With `DTO_STATS_INTERVAL_MS=N`, a background thread also writes, every N ms, +what happened since the previous record, so activity can be followed over +time. The interval records add up to the summary's totals. + +With `DTO_STATS_FORMAT=json` the output is JSON Lines, one record per line: + +``` +{"type":"interval","pid":183654,"t_ms":14.333,"dt_ms":14.333,"ops":{"memcpy":{"dsa":{"calls":8,"bytes":1049560,"ns":96273}},"memmove":{"cpu":{"calls":8,"bytes":786488,"ns":31601}}},"failures":{"retry":0,"page_fault":0,"other":0}} +{"type":"summary","pid":183654,"program":"prog","run_ms":23.395, + "config":{"stdc_only":0,"wait_method":"busypoll","min_bytes":65536,"cpu_size_fraction":0.00,"auto_adjust":1,"numa_aware":0,"wqs":1}, + "totals":{"memcpy":{"dsa":{"calls":12,"bytes":1574340,"ns":138591}},...}, + "failures":{"retry":0,"page_fault":0,"other":0}, + "histogram":[{"min":131072,"max":163839,"ops":{"memcpy":{"dsa":{"calls":12,"bytes":1574340,"ns":138591}}},"failures":{...}},...]} +``` + +(The summary is a single line; it is wrapped here.) `ops` maps `memset`, +`memcpy`, `memmove` and `memcmp` to the paths they took, `cpu`, `dsa` and +`dsa_failed`, each with the number of calls, bytes and total nanoseconds; +operations and paths with no calls are left out. `t_ms` and `run_ms` count +from DTO's start, and `config` holds the settings at exit, including the +auto-tuned `min_bytes` and `cpu_size_fraction`. + +To compare a run with DSA against a CPU-only run, and see how close the speedup +comes to what Amdahl's law predicts, use `dtoctl --stats-run` (see +[Measuring the speedup from DSA](#measuring-the-speedup-from-dsa)). + +Every process that loads DTO with stats enabled writes its own records, +tagged with its `pid`. They share `DTO_STATS_FILE` (records are appended +whole) unless the path contains `%p`, which gives each process its own file. ## Usage Notes diff --git a/TESTING.md b/TESTING.md index d0e1b37..bf2ef7b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -20,7 +20,9 @@ cmake --build build cd build ``` -To disable tests: `-DDTO_BUILD_TESTS=OFF` +To disable tests: `-DDTO_BUILD_TESTS=OFF`. When DTO is built as part of +another CMake project (`add_subdirectory` or `FetchContent`) the tests are off +unless `DTO_BUILD_TESTS` is set. ## Running Tests @@ -79,6 +81,41 @@ the work queue has (it reports which), so run the suite once with `block_on_fault` enabled and once without to cover both. It assumes all work queues DTO uses share the same setting. +## dtoctl Tests + +Checks for the `dtoctl` wrapper: every option sets its `DTO_*` variable, +invalid values are rejected, the library path is resolved and validated, +`LD_PRELOAD` and inherited variables are handled, and exit statuses follow +`env(1)`. `--stats-run` is checked from its `--dry-run` plan and by running a +small shell command: each run's environment and stats file, `runs.jsonl`, the +report, and stopping at a failed run. The last check runs a program under +`dtoctl` and reads libdto's own configuration trace. No DSA hardware is needed. + +```bash +ctest -R dtoctl +# or directly, from the source directory +tests/test_dtoctl.sh build/dtoctl build/libdto.so +``` + +## Stats Tests + +`stats` runs `dto-test-stats-workload`, which makes a known number of calls +of known sizes, with `DTO_COLLECT_STATS` set, and checks the output: totals +and histogram buckets, interval records adding up to the totals, calls from +200 short-lived threads all counted without their stats memory piling up, +separate records for a forked child, `%p` file names, and the text format. +It needs Python 3 and no DSA hardware. + +```bash +ctest -R stats +``` + +`stats-report` checks `dto-stats-report` on made-up runs with known answers +(Amdahl's law, per-operation and per-size speedups, medians, the notes it +makes), sorts made-up perf samples into categories, and checks that it finds +memcpy, memset and memcmp by address in this machine's libc. It needs Python 3, +and neither DSA nor perf. + ## Performance Tests The performance test (`perf`) detects regressions in libdto by diff --git a/doc/dtoctl.md b/doc/dtoctl.md new file mode 100644 index 0000000..be73825 --- /dev/null +++ b/doc/dtoctl.md @@ -0,0 +1,354 @@ +% DTOCTL(1) Dtoctl User Manuals +% Honggyu Kim +% 2026 + +NAME +==== +dtoctl - run a program with DSA Transparent Offload (DTO) preloaded + + +SYNOPSIS +======== +dtoctl [_options_] COMMAND [_command-options_] + +dtoctl \--dry-run [_options_] [COMMAND [_command-options_]] + +dtoctl \--stats-run[=_DIR_] [\--repeat=_N_] [\--perf[=_PERF_]] [_options_] COMMAND [_command-options_] + + +DESCRIPTION +=========== +The **dtoctl** tool runs `COMMAND` with the DSA Transparent Offload library +(**libdto.so**) preloaded, so that its `memcpy`, `memmove`, `memset`, and +`memcmp` calls are transparently offloaded to Intel DSA (Data Streaming +Accelerator). + +Without **dtoctl**, users must set **LD_PRELOAD** to point at **libdto.so** and +export a set of **DTO_\*** environment variables to control DTO behavior. +**dtoctl** replaces that manual setup: it checks its command line options, +converts them into the corresponding **DTO_\*** environment variables, prepends +**libdto.so** to **LD_PRELOAD**, and then executes `COMMAND`. Nothing is left +behind in the calling shell. + +Only the options that are explicitly given are applied. Options that are not +given leave any pre-existing **DTO_\*** environment variables (or the DTO +built-in defaults) untouched, and an explicitly given option overrides a +previously exported variable of the same name. Use `-i` to ignore the +inherited **DTO_\*** variables instead. + +Option values are validated before `COMMAND` runs, and an invalid value is an +error. DTO itself silently falls back to a default when a variable holds a +value it does not understand. + +`COMMAND` and every process it starts inherit the settings, so +`dtoctl make` runs each compiler under DTO as well. + + +LIBRARY RESOLUTION +================== +The path of **libdto.so** to preload is resolved in the following order: + +1. The `-l`, `--library` option, if given. +2. The **DTO_LIBRARY** environment variable, if set and not empty. This + variable is read by **dtoctl** only, not by **libdto.so**. +3. For a CMake build, the **libdto.so** installed with **dtoctl**, found + relative to the **dtoctl** executable (`/../lib/libdto.so`), so + that an installation moved with `cmake --install --prefix` finds its own + library. +4. The compiled-in default path (`/usr/local/lib/libdto.so` for a default + CMake build, or `/usr/lib64/libdto.so` for the Makefile build). + +The resolved path is made absolute, so it keeps working when `COMMAND` changes +directory, and **dtoctl** fails with exit status 125 if it cannot read the +library. (**ld.so** would only print a warning and run `COMMAND` without DTO.) +Run `dtoctl --dry-run` to see which library was chosen and why. + +The library is prepended to any existing **LD_PRELOAD** value. Any other +**libdto.so** entry already in **LD_PRELOAD** is removed, with a warning when it +names a different file, so that one process never loads two copies of DTO. + + +OPTIONS +======= + +General +------- +-l _PATH_, \--library=_PATH_ +: Path to **libdto.so** to prepend to **LD_PRELOAD**. Overrides + **DTO_LIBRARY** and the installed library (see LIBRARY RESOLUTION). + +-i, \--ignore-env +: Unset every **DTO_\*** variable inherited from the environment, including + **DTO_LIBRARY**, so that only the options given on this command line and + the DTO built-in defaults apply. This is the way to undo an exported + variable that the `--no-*` options cannot, such as + **DTO_USESTDC_CALLS=1** or **DTO_DSA_MEMCPY=0**. + +\--dry-run +: Print the resolved library and where it came from, the resulting + **LD_PRELOAD**, every **DTO_\*** variable that `COMMAND` would see (each + marked `option` or `inherited`), and the command line, then exit without + running anything. `COMMAND` may be omitted. + +Offload tuning +-------------- +-w _METHOD_, \--wait-method=_METHOD_ +: How to wait for DSA completion: _yield_, _busypoll_, _umwait_, or _tpause_. + _umwait_ and _tpause_ need a CPU with WAITPKG support; DTO falls back + otherwise. Sets **DTO_WAIT_METHOD**. Default: _busypoll_. + +-b _BYTES_, \--min-bytes=_BYTES_ +: Minimum operation size offloaded to DSA. Smaller operations run on the CPU. + _BYTES_ may have a _K_, _M_, or _G_ suffix (powers of 1024), and is passed to + DTO as a plain byte count. With auto tuning on, this is the starting + value. Sets **DTO_MIN_BYTES**. Default: 64K. + +-c _FRACTION_, \--cpu-fraction=_FRACTION_ +: Fraction (0.0 <= f < 1.0) of each operation performed on the CPU in parallel + to DSA. DTO uses two decimal places. With auto tuning on, this is the + starting value. Sets **DTO_CPU_SIZE_FRACTION**. Default: 0.0. + +-n _MODE_, \--numa-aware=_MODE_ +: NUMA awareness: _0_ disable, _1_ buffer-centric, _2_ cpu-centric. Has no + effect if DTO was built without NUMA support. Sets **DTO_IS_NUMA_AWARE**. + Default: _0_. + +-q _LIST_, \--wq-list=_LIST_ +: Semicolon-separated list of DSA work queues to use, e.g. "wq0.0;wq2.0". + Names must match those under /dev/dsa/, and the list can be at most 255 + characters. Required if DTO was built without accel-config support. + Sets **DTO_WQ_LIST**. Default: auto-discover all available WQs. + +\--umwait-delay=_CYCLES_ +: Delay in cycles for the umwait instruction, a positive number. The + maximum is in /sys/devices/system/cpu/umwait_control/max_time. Sets + **DTO_UMWAIT_DELAY**. Default: 100000. + +\--overlapping-memmove=_WHERE_ +: Where to run memmove with overlapping buffers: _cpu_ or _dsa_. + Sets **DTO_OVERLAPPING_MEMMOVE_ACTION** (cpu -> 0, dsa -> 1). Default: _cpu_. + +Disable DSA offload for specific operations +------------------------------------------- +By default every one of these operations is offloaded to DSA; each option below +turns a single one back into its standard C library call on the CPU. + +\--no-memcpy +: Use the system memcpy instead of DSA. Sets **DTO_DSA_MEMCPY=0**. + +\--no-memmove +: Use the system memmove instead of DSA. Sets **DTO_DSA_MEMMOVE=0**. + +\--no-memset +: Use the system memset instead of DSA. Sets **DTO_DSA_MEMSET=0**. + +\--no-memcmp +: Use the system memcmp instead of DSA. Sets **DTO_DSA_MEMCMP=0**. + +Other toggles +------------- +\--no-cache-control +: Clear the DSA cache control flag to avoid cache pollution. + Sets **DTO_DSA_CC=0**. Default: cache control is on. + +\--no-auto-adjust +: Disable auto tuning of cpu-fraction and min-bytes. + Sets **DTO_AUTO_ADJUST_KNOBS=0**. Default: auto tuning is on. + +\--stdc-only +: Use only the standard C memory functions, no DSA offload. + Sets **DTO_USESTDC_CALLS=1**. Default: DSA offload is enabled. + +Statistics and logging +---------------------- +\--stats +: Enable stats collection: calls, bytes and time for each operation, by + path (CPU, DSA, DSA failed) and by size. Costs about 40 ns per call. + Sets **DTO_COLLECT_STATS=1**. Default: off. + +\--stats-file=_PATH_ +: Write the stats to _PATH_ instead of standard output; each `%p` in _PATH_ + becomes the process ID. The file is appended to. Implies `--stats`, + since DTO ignores the file otherwise. Sets **DTO_STATS_FILE** and + **DTO_COLLECT_STATS=1**. Default: standard output. + +\--stats-format=_FORMAT_ +: _text_ for tables, or _json_ for JSON Lines (one record per line). + Implies `--stats`. Sets **DTO_STATS_FORMAT**. Default: _text_. + +\--stats-interval=_MS_ +: Also write the stats for every _MS_ milliseconds, from a background + thread, as well as the summary at exit. Implies `--stats`. Sets + **DTO_STATS_INTERVAL_MS**. Default: summary only. + +\--log-file=_PATH_ +: Redirect DTO output to _PATH_._program_._pid_, one file per process. + Sets **DTO_LOG_FILE**. Default: standard output. + +\--log-level=_LEVEL_ +: Verbosity of DTO logging: _0_, _1_, or _2_. Sets **DTO_LOG_LEVEL**. + Default: _0_. + +Stats run +--------- +\--stats-run[=_DIR_] +: Measure how much DSA speeds `COMMAND` up. Run it CPU-only + (**DTO_USESTDC_CALLS=1**), then with DSA (**DTO_USESTDC_CALLS=0**), + collecting JSON stats from each run into _DIR_, then summarize them with + **dto-stats-report** (see STATS RUN). _DIR_ must be new or empty; the + default is a new `dto-stats--