From: Partha Satapathy <partha.satapathy@oracle.com>
To: Jan Kara <jack@suse.cz>
Cc: amir73il@gmail.com, viro@zeniv.linux.org.uk, brauner@kernel.org,
linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [External] : Re: [PATCH 0/1] fsnotify: Check parent watches without child dentry flags
Date: Tue, 13 Oct 2026 12:08:54 +0530 [thread overview]
Message-ID: <2528e790-8913-45dc-a8d5-cc22908a441e@oracle.com> (raw)
In-Reply-To: <gnntcprg4f7ag7ictugnfza24nwfdmyubkuqac7plxbwtedfrz@xgxyvks3h2uv>
On 12-10-2026 14:10, Jan Kara wrote:
> On Fri 09-10-26 17:18:17, Partha Satapathy wrote:
>> Hi Honza,
>>
>> Thanks for suggesting this workload.
>> The setup and before/after timing comparisons are below.
>>
>> Architecture: x86_64
>> CPU(s): 512
>> On-line CPU(s) list: 0-511
>>
>> NUMA:
>> NUMA node(s): 2
>> NUMA node0 CPU(s): 0-127,256-383
>> NUMA node1 CPU(s): 128-255,384-511
>>
>> Testcase :
>> Using /dev/shm tmpfs to create private test directory.
>> Create 1,024 files each fully populated with 4kb.
>> Add IN_MODIFY inotify watch to the directory.
>> Start a listener thread to read notifications and count queue overflows.
>> Start X(1,2,4..1024) client threads; each opens a different file once.
>> Wait until every client is ready, then release them together.
>> Each client starts its timer and performs 1000000
>> one-byte pwrite() at offset0 on its own file.
>> Each client stops its timer and closes its file.
>> Wait for all clients to finish, drain remaining notifications,
>> and stop the listener.
>> Print write times, throughput, notification count, and queue-overflow count.
>> Close inotify and delete the test files and directory.
>>
>> Note : File creation, open, close, and cleanup are outside the timed
>> write loops.
>>
>> Run 1:
>> -------
>>
>> | Clients | Before average time | After average time | Time change |
>> |---:|---:|---:|---:|
>> | 1 | 1.392 s | 1.484 s | +6.6% |
>> | 2 | 2.205 s | 2.402 s | +8.9% |
>> | 4 | 3.969 s | 3.644 s | −8.2% |
>> | 8 | 5.851 s | 5.804 s | −0.8% |
>> | 16 | 10.607 s | 11.538 s | +8.8% |
>> | 32 | 21.093 s | 22.492 s | +6.6% |
>> | 64 | 48.698 s | 49.576 s | +1.8% |
>> | 128 | 100.208 s | 100.467 s | +0.3% |
>> | 256 | 198.773 s | 197.669 s | −0.6% |
>> | 512 | 400.056 s | 399.550 s | −0.1% |
>>
>>
>> Run2 :
>> --------
>>
>> | Clients | Before average time | After average time | Time change |
>> |---:|---:|---:|---:|
>> | 1 | 1.491 s | 1.484 s | −0.4% |
>> | 2 | 2.391 s | 2.402 s | +0.5% |
>> | 4 | 4.050 s | 3.644 s | −10.0% |
>> | 8 | 6.630 s | 5.804 s | −12.5% |
>> | 16 | 11.931 s | 11.538 s | −3.3% |
>> | 32 | 22.513 s | 22.492 s | −0.1% |
>> | 64 | 50.231 s | 49.576 s | −1.3% |
>> | 128 | 100.699 s | 100.467 s | −0.2% |
>> | 256 | 198.771 s | 197.669 s | −0.6% |
>> | 512 | 401.860 s | 399.550 s | −0.6% |
>> | 1,024 | 803.812 s | 806.593 s | +0.3% |
>>
>>
>> Positive means After Fix took longer; negative means faster.
>>
>>
>> The measured write performance is broadly comparable before and after
>> the fix, with no consistent substantial regression or progressively
>> worsening slowdown as concurrency increases.
>>
>> The differences vary between baseline batches: the increases at 16
>> and 32 clients in the first comparison disappear or reverse in the
>> second. At 128–1024 clients, average write times differ by approximately
>> 0.6% or less. These shifts are consistent with run-to-run variation;
>> the results do not show a persistent substantial slowdown from the fix.
>
> Thanks for running these tests. Results are very interesting. Can you
> please share your benchmarking program with me? I'll check it and try to
> play with it on my end. Thanks!
>
> Honza
> --
> Jan Kara <jack@suse.com>
> SUSE Labs, CR
>
$ cat run_parent_ref_scale.sh
#!/usr/bin/env bash
set -u -o pipefail
bench=./fsnotify_parent_ref_bench
if [[ ! -x "$bench" ]]; then
printf 'Build %s in the current directory first.\n' "$bench" >&2
exit 1
fi
results_dir=$(mktemp -d ./parent-ref-results-XXXXXXXX) || exit 1
printf
'kernel=%s\nbenchmark=%s\ntmpfs=/dev/shm\nwrites_per_client=1000000\nmode=watch\n'
\
"$(uname -r)" "$bench" > "$results_dir/run-info.txt"
printf 'Results: %s\n' "$results_dir"
for i in 1 2 3 4 5; do
for clients in 1 2 4 8 16 32 64 128 256 512 1024; do
log="$results_dir/clients-${clients}.log"
printf '\n=== clients=%d ===\n' "$clients"
"$bench" /dev/shm "$clients" 1000000 watch 2>&1 | tee "$log"
status=${PIPESTATUS[0]}
printf 'exit_status=%d\n' "$status" | tee -a "$log"
done
done
printf '\nAll results saved in %s\n' "$results_dir"
$ cat fsnotify_parent_ref_bench.c
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <time.h>
#include <unistd.h>
#include <linux/magic.h>
#define FILES 1024
#define FILE_SIZE 4096
#define DEFAULT_WRITES 1000000UL
struct gate {
pthread_mutex_t mutex;
pthread_cond_t cond;
unsigned int ready;
bool start;
bool abort;
};
struct worker {
char path[PATH_MAX];
unsigned long writes;
unsigned int id;
struct gate *gate;
uint64_t begin_ns;
uint64_t end_ns;
int error;
};
struct listener {
int fd;
atomic_bool stop;
uint64_t events;
uint64_t overflows;
int error;
};
static uint64_t now_ns(void)
{
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts)) {
perror("clock_gettime");
exit(EXIT_FAILURE);
}
return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}
static unsigned long parse_count(const char *s, const char *name)
{
char *end;
unsigned long value;
errno = 0;
value = strtoul(s, &end, 10);
if (errno || !*s || *end || !value) {
fprintf(stderr, "Invalid %s: %s\n", name, s);
exit(EXIT_FAILURE);
}
return value;
}
static void file_path(char *out, size_t size, const char *dir, unsigned
int i)
{
int n = snprintf(out, size, "%s/file-%04u", dir, i);
if (n < 0 || (size_t)n >= size) {
fputs("File path is too long\n", stderr);
exit(EXIT_FAILURE);
}
}
static void *run_worker(void *arg)
{
struct worker *w = arg;
int fd;
char byte = (char)w->id;
unsigned long i;
bool abort;
fd = open(w->path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
w->error = errno;
pthread_mutex_lock(&w->gate->mutex);
w->gate->ready++;
pthread_cond_broadcast(&w->gate->cond);
while (!w->gate->start)
pthread_cond_wait(&w->gate->cond, &w->gate->mutex);
abort = w->gate->abort;
pthread_mutex_unlock(&w->gate->mutex);
if (abort || w->error)
goto out;
w->begin_ns = now_ns();
for (i = 0; i < w->writes; i++) {
if (pwrite(fd, &byte, 1, 0) != 1) {
w->error = errno ? errno : EIO;
break;
}
}
w->end_ns = now_ns();
out:
if (fd >= 0 && close(fd) && !w->error)
w->error = errno;
return NULL;
}
static void *drain_events(void *arg)
{
struct listener *l = arg;
char buf[65536] __attribute__((aligned(8)));
struct pollfd pfd = { .fd = l->fd, .events = POLLIN };
for (;;) {
int ready = poll(&pfd, 1, atomic_load(&l->stop) ? 0 : 50);
if (ready < 0) {
if (errno == EINTR)
continue;
l->error = errno;
break;
}
if (!ready) {
if (atomic_load(&l->stop))
break;
continue;
}
for (;;) {
ssize_t len = read(l->fd, buf, sizeof(buf));
size_t pos = 0;
if (len < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN)
break;
l->error = errno;
return NULL;
}
while (pos + sizeof(struct inotify_event) <=
(size_t)len) {
struct inotify_event *ev =
(struct inotify_event *)(buf + pos);
size_t step = sizeof(*ev) + ev->len;
if (step > (size_t)len - pos) {
l->error = EPROTO;
return NULL;
}
l->events++;
if (ev->mask & IN_Q_OVERFLOW)
l->overflows++;
pos += step;
}
if (pos != (size_t)len) {
l->error = EPROTO;
return NULL;
}
}
}
return NULL;
}
static void cleanup(const char *dir)
{
char path[PATH_MAX];
unsigned int i;
for (i = 0; i < FILES; i++) {
file_path(path, sizeof(path), dir, i);
if (unlink(path) && errno != ENOENT)
perror(path);
}
if (rmdir(dir))
perror(dir);
}
int main(int argc, char **argv)
{
char dir[PATH_MAX], path[PATH_MAX];
unsigned long writes = DEFAULT_WRITES;
unsigned long count;
unsigned int clients, i, created = 0;
struct statfs fs;
struct gate gate = {
.mutex = PTHREAD_MUTEX_INITIALIZER,
.cond = PTHREAD_COND_INITIALIZER,
};
struct listener listener = { .fd = -1 };
struct worker *workers;
pthread_t *threads, listener_thread;
bool watched = true, listener_started = false;
uint64_t first = UINT64_MAX, last = 0, sum = 0;
uint64_t shortest = UINT64_MAX, longest = 0;
char zeroes[FILE_SIZE] = { 0 };
int rc = EXIT_FAILURE, err;
if (argc < 3 || argc > 5) {
fprintf(stderr, "Usage: %s TMPFS_ROOT CLIENTS
[WRITES_PER_CLIENT] [watch|none]\n", argv[0]);
return rc;
}
clients = parse_count(argv[2], "clients");
if (clients > FILES) {
fprintf(stderr, "Clients must be at most %d\n", FILES);
return rc;
}
if (argc > 3)
writes = parse_count(argv[3], "writes per client");
if (writes > ULONG_MAX / clients) {
fputs("Total write count is too large\n", stderr);
return rc;
}
if (argc > 4) {
if (!strcmp(argv[4], "none"))
watched = false;
else if (strcmp(argv[4], "watch")) {
fputs("Mode must be watch or none\n", stderr);
return rc;
}
}
if (statfs(argv[1], &fs)) {
perror(argv[1]);
return rc;
}
if (fs.f_type != TMPFS_MAGIC) {
fprintf(stderr, "%s must be a tmpfs directory\n", argv[1]);
return rc;
}
if (snprintf(dir, sizeof(dir), "%s/fsnotify-parent-ref-XXXXXX",
argv[1]) >=
(int)sizeof(dir) || !mkdtemp(dir)) {
perror("mkdtemp");
return rc;
}
workers = calloc(clients, sizeof(*workers));
threads = calloc(clients, sizeof(*threads));
if (!workers || !threads) {
fputs("Cannot allocate worker state\n", stderr);
goto out;
}
for (i = 0; i < FILES; i++) {
int fd;
file_path(path, sizeof(path), dir, i);
fd = open(path, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC,
0600);
if (fd < 0) {
perror(path);
goto out;
}
/* Allocate the tmpfs page before the measured write
loop. */
if (write(fd, zeroes, sizeof(zeroes)) != sizeof(zeroes)) {
perror(path);
close(fd);
goto out;
}
if (close(fd)) {
perror(path);
goto out;
}
}
if (watched) {
listener.fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (listener.fd < 0 ||
inotify_add_watch(listener.fd, dir, IN_MODIFY) < 0) {
perror("inotify watch");
goto out;
}
err = pthread_create(&listener_thread, NULL,
drain_events, &listener);
if (err) {
errno = err;
perror("listener thread");
goto out;
}
listener_started = true;
}
for (i = 0; i < clients; i++) {
workers[i].id = i;
workers[i].writes = writes;
workers[i].gate = &gate;
file_path(workers[i].path, sizeof(workers[i].path), dir, i);
err = pthread_create(&threads[i], NULL, run_worker,
&workers[i]);
if (err) {
errno = err;
perror("worker thread");
gate.abort = true;
goto release;
}
created++;
}
pthread_mutex_lock(&gate.mutex);
while (gate.ready < clients)
pthread_cond_wait(&gate.cond, &gate.mutex);
pthread_mutex_unlock(&gate.mutex);
for (i = 0; i < clients; i++) {
if (workers[i].error) {
fprintf(stderr, "Worker %u open: %s\n", i,
strerror(workers[i].error));
gate.abort = true;
break;
}
}
printf("test_dir=%s files=%d file_size=%d clients=%u
writes_per_client=%lu mode=%s\n",
dir, FILES, FILE_SIZE, clients, writes,
watched ? "watch" : "none");
fflush(stdout);
release:
pthread_mutex_lock(&gate.mutex);
gate.start = true;
pthread_cond_broadcast(&gate.cond);
pthread_mutex_unlock(&gate.mutex);
for (i = 0; i < created; i++)
pthread_join(threads[i], NULL);
if (listener_started) {
atomic_store(&listener.stop, true);
pthread_join(listener_thread, NULL);
printf("inotify_events=%" PRIu64 " queue_overflows=%"
PRIu64 "\n",
listener.events, listener.overflows);
if (listener.error)
fprintf(stderr, "Inotify listener: %s\n",
strerror(listener.error));
if (listener.overflows)
fputs("Inotify queue overflowed; event delivery
load changed during the run\n", stderr);
}
if (gate.abort || listener.error)
goto out;
for (i = 0; i < clients; i++) {
uint64_t duration;
if (workers[i].error) {
fprintf(stderr, "Worker %u write: %s\n", i,
strerror(workers[i].error));
goto out;
}
duration = workers[i].end_ns - workers[i].begin_ns;
if (workers[i].begin_ns < first)
first = workers[i].begin_ns;
if (workers[i].end_ns > last)
last = workers[i].end_ns;
if (duration < shortest)
shortest = duration;
if (duration > longest)
longest = duration;
sum += duration;
}
count = (unsigned long)clients * writes;
printf("total_writes=%lu wall_seconds=%.6f
throughput_writes_per_sec=%.0f mean_worker_ns_per_write=%.1f
shortest_worker_seconds=%.6f longest_worker_seconds=%.6f\n",
count, (last - first) / 1e9, count * 1e9 / (last - first),
(double)sum / count, shortest / 1e9, longest / 1e9);
printf("result=%s\n", listener.overflows ? "QUEUE_OVERFLOW" :
"PASS");
rc = listener.overflows ? EXIT_FAILURE : EXIT_SUCCESS;
out:
if (listener.fd >= 0)
close(listener.fd);
cleanup(dir);
free(workers);
free(threads);
return rc;
}
Please compile : fsnotify_parent_ref_bench.c -o
fsnotify_parent_ref_bench. After that
run the script ./run_parent_ref_scale.sh
Thanks
Partha
prev parent reply other threads:[~2026-10-13 6:39 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-07 15:20 Partha Sarathi Satapathy
2026-10-07 15:20 ` [PATCH 1/1] " Partha Sarathi Satapathy
2026-10-08 9:42 ` [PATCH 0/1] " Jan Kara
2026-10-09 11:48 ` [External] : " Partha Satapathy
2026-10-12 8:40 ` Jan Kara
2026-10-13 6:38 ` Partha Satapathy [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=2528e790-8913-45dc-a8d5-cc22908a441e@oracle.com \
--to=partha.satapathy@oracle.com \
--cc=amir73il@gmail.com \
--cc=brauner@kernel.org \
--cc=jack@suse.cz \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®