Created
July 6, 2026 21:06
-
-
Save teknoraver/67b50ce366d7cf430bb7f82a8018acfa to your computer and use it in GitHub Desktop.
XFS truncate after clone PoC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* clone_bench - measure the per-file cost of FICLONERANGE on many | |
| small files, compared to a plain copy. | |
| Creates NFILES files of SIZE bytes each, in the current directory, | |
| from a common source file, in one of three ways: | |
| write plain write(2) of the whole content | |
| clone-trunc FICLONERANGE of size rounded up to 4k, then | |
| ftruncate to the real size (what a program cloning | |
| a non-block-aligned range out of a larger file, | |
| like tar --reflink, has to do) | |
| clone-tail FICLONERANGE of the whole 4k blocks, then write(2) | |
| of the final partial block (no truncation) | |
| write-trunc write(2) of size rounded up to 4k, then ftruncate | |
| to the real size (same shrink as clone-trunc but on | |
| unshared extents) | |
| Prints the loop time, the syncfs time (deferred writeback and log | |
| traffic), and the per-file cost broken down by syscall. | |
| gcc -O2 -o clone_bench clone_bench.c | |
| */ | |
| #define _GNU_SOURCE | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <fcntl.h> | |
| #include <unistd.h> | |
| #include <time.h> | |
| #include <sys/ioctl.h> | |
| #include <linux/fs.h> | |
| #define BLOCK 4096L | |
| static double now(void) | |
| { | |
| struct timespec ts; | |
| clock_gettime(CLOCK_MONOTONIC, &ts); | |
| return ts.tv_sec + ts.tv_nsec / 1e9; | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| if (argc != 4) { | |
| fprintf(stderr, "usage: %s write|clone-trunc|clone-tail|write-trunc <nfiles> <size>\n", argv[0]); | |
| return 1; | |
| } | |
| const char *mode = argv[1]; | |
| long n = atol(argv[2]); | |
| long size = atol(argv[3]); | |
| long whole = size / BLOCK * BLOCK; | |
| long src_len = (size + BLOCK - 1) / BLOCK * BLOCK; | |
| char *buf = malloc(src_len); | |
| memset(buf, 'x', src_len); | |
| int src = open("src.dat", O_RDWR | O_CREAT | O_TRUNC, 0644); | |
| if (src < 0 || write(src, buf, src_len) != src_len || fsync(src) < 0) { | |
| perror("src.dat"); | |
| return 1; | |
| } | |
| double t_open = 0, t_data = 0, t_trunc = 0, t_close = 0; | |
| double t0 = now(); | |
| for (long i = 0; i < n; i++) { | |
| char name[64]; | |
| double ta, tb, tc; | |
| snprintf(name, sizeof name, "f%06ld", i); | |
| ta = now(); | |
| int fd = open(name, O_WRONLY | O_CREAT | O_TRUNC, 0644); | |
| if (fd < 0) { | |
| perror(name); | |
| return 1; | |
| } | |
| tb = now(); | |
| t_open += tb - ta; | |
| if (!strcmp(mode, "write")) { | |
| if (write(fd, buf, size) != size) { | |
| perror("write"); | |
| return 1; | |
| } | |
| tc = now(); | |
| t_data += tc - tb; | |
| } else if (!strcmp(mode, "write-trunc")) { | |
| if (write(fd, buf, src_len) != src_len) { | |
| perror("write"); | |
| return 1; | |
| } | |
| tc = now(); | |
| t_data += tc - tb; | |
| if (ftruncate(fd, size) < 0) { | |
| perror("ftruncate"); | |
| return 1; | |
| } | |
| tb = now(); | |
| t_trunc += tb - tc; | |
| tc = tb; | |
| } else { | |
| long clone_len = !strcmp(mode, "clone-trunc") ? src_len : whole; | |
| struct file_clone_range fcr = { | |
| .src_fd = src, | |
| .src_offset = 0, | |
| .src_length = clone_len, | |
| }; | |
| if (clone_len && ioctl(fd, FICLONERANGE, &fcr) < 0) { | |
| perror("FICLONERANGE"); | |
| return 1; | |
| } | |
| tc = now(); | |
| t_data += tc - tb; | |
| if (!strcmp(mode, "clone-trunc")) { | |
| if (ftruncate(fd, size) < 0) { | |
| perror("ftruncate"); | |
| return 1; | |
| } | |
| } else if (whole < size) { | |
| if (lseek(fd, whole, SEEK_SET) < 0 | |
| || write(fd, buf + whole, size - whole) != size - whole) { | |
| perror("tail write"); | |
| return 1; | |
| } | |
| } | |
| tb = now(); | |
| t_trunc += tb - tc; | |
| tc = tb; | |
| } | |
| close(fd); | |
| t_close += now() - tc; | |
| } | |
| double t1 = now(); | |
| int dirfd = open(".", O_RDONLY); | |
| if (dirfd < 0 || syncfs(dirfd) < 0) { | |
| perror("syncfs"); | |
| return 1; | |
| } | |
| double t2 = now(); | |
| printf("%-12s n=%ld size=%ld loop=%6.3fs sync=%6.3fs total=%6.3fs us/file: open=%.1f %s=%.1f %s=%.1f close=%.1f\n", | |
| mode, n, size, t1 - t0, t2 - t1, t2 - t0, | |
| t_open * 1e6 / n, | |
| !strncmp(mode, "write", 5) ? "write" : "clone", t_data * 1e6 / n, | |
| strstr(mode, "trunc") ? "trunc" : "tail", t_trunc * 1e6 / n, | |
| t_close * 1e6 / n); | |
| return 0; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # usage: ./run.sh <dir-on-target-fs> [nfiles] | |
| # Runs the write / clone-tail / clone-trunc matrix over a few file | |
| # sizes, each in a fresh subdirectory, and prints system info for the | |
| # bug report. | |
| set -e | |
| dir=${1:?usage: $0 <dir-on-target-fs> [nfiles]} | |
| n=${2:-20000} | |
| bench=$(cd "$(dirname "$0")" && pwd)/clone_bench | |
| uname -r | |
| grep -w "$(stat -f -c %T "$dir" >/dev/null 2>&1; df --output=source "$dir" | tail -1)" /proc/mounts || true | |
| stat -f -c 'fstype=%T bsize=%S' "$dir" | |
| command -v xfs_info >/dev/null && xfs_info "$dir" 2>/dev/null | head -6 || true | |
| echo | |
| for size in 1000 5000 20000 65536 90000; do | |
| for mode in write write-trunc clone-tail clone-trunc; do | |
| sub=$dir/bench-$mode-$size | |
| rm -rf "$sub"; mkdir -p "$sub" | |
| (cd "$sub" && "$bench" "$mode" "$n" "$size") | |
| rm -rf "$sub" | |
| sync | |
| done | |
| echo | |
| done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment