diff --git a/CMakeLists.txt b/CMakeLists.txt index b7a9139..2c83353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,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) @@ -157,6 +160,11 @@ else() 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) @@ -221,6 +229,33 @@ if(DTO_BUILD_TESTS) 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 diff --git a/Makefile b/Makefile index 25b649a..15467f5 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ MANDIR ?= /usr/share/man 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 @@ -43,6 +43,7 @@ install: dtoctl install-man 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 \ diff --git a/README.md b/README.md index 71e1424..bed25a0 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,12 @@ 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. + 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 @@ -192,6 +195,89 @@ 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. @@ -310,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 db6b543..bf2ef7b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -86,8 +86,10 @@ queues DTO uses share the same setting. 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)`. The last check runs a program under `dtoctl` and reads libdto's own -configuration trace. No DSA hardware is needed. +`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 @@ -95,6 +97,25 @@ ctest -R dtoctl 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 index afca4ed..be73825 100644 --- a/doc/dtoctl.md +++ b/doc/dtoctl.md @@ -13,6 +13,8 @@ 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 =========== @@ -160,13 +162,24 @@ Other toggles Statistics and logging ---------------------- \--stats -: Enable stats collection. For debugging/profiling only, as it slows down the - workload. Sets **DTO_COLLECT_STATS=1**. Default: off. +: 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 histogram to _PATH_ instead of standard output. Implies - `--stats`, since DTO ignores the file otherwise. Sets **DTO_STATS_FILE** - and **DTO_COLLECT_STATS=1**. Default: standard output. +: 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. @@ -176,6 +189,28 @@ Statistics and logging : 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--