diff --git a/patches/fuse-spin-wait.patch b/patches/fuse-spin-wait.patch new file mode 100644 index 0000000..b44bd6c --- /dev/null +++ b/patches/fuse-spin-wait.patch @@ -0,0 +1,78 @@ +perf(fuse): bounded spin-wait for metadata request completion + +On virtualized ARM64 every reschedule IPI is a trap: the architecture +has no TIF_POLLING_NRFLAG, so try_to_wake_up always sends a physical +IPI, and with all vCPUs in one LLC domain wake_affine never migrates +the waiter onto the IRQ CPU. A synchronous FUSE workload therefore pays +device-IRQ trap + IPI trap + two context switches per operation — the +reschedule-IPI count matches the virtio-fs request IRQ count 1:1 +(ArcBox CORE-48 measurements, Apple Virtualization.framework guest). + +Metadata completions typically arrive within ~20-30us, so poll +FR_FINISHED for up to 40us before sleeping — KVM halt polling, applied +at the FUSE wait point. The waiter observes the completion while still +runnable: no wakeup IPI, no context switches. need_resched() breaks the +spin immediately when the CPU has real work. + +Two gates bound the cost. Request types that legitimately block (bulk +READ/WRITE, flush/fsync, copy_file_range, blocking locks) never spin: +their replies outlive any sane budget, and an ungated spin cost -54% +sequential-read throughput in A/B testing. Classic /dev/fuse transports +(userspace daemons, arbitrary latency) never spin either — only +kernel-mediated transports such as virtio-fs, where completion is +IRQ-driven and fast, take the poll path. With the gates the win is +one-sided +(same guest, same VM, metadata_stat ops/s): + + stock 6.18.38 31.8k (sequential read 3771 MB/s) + + this patch 50.3k (sequential read 3637 MB/s, noise) + hard-pinned ceiling 54.6k (taskset + IRQ affinity, reference) + +negative_lookup +40%, create_delete +28%. A fixed budget keeps the +patch minimal; an adaptive grow/shrink budget (as in KVM halt polling) +is the natural upstream refinement. + +--- a/fs/fuse/dev.c ++++ b/fs/fuse/dev.c +@@ -25,2 +25,3 @@ + #include ++#include + #include +@@ -547,6 +548,37 @@ static void request_wait_answer(struct fuse_req *req) + struct fuse_iqueue *fiq = &fc->iq; + int err; + ++ /* ++ * Poll briefly before sleeping: on kernel-mediated transports ++ * (virtio-fs) metadata completions typically arrive within ++ * ~20-30us, and observing the flag while still runnable avoids ++ * the reschedule IPI (a trap on virtualized ARM64) and both ++ * context switches. Classic /dev/fuse daemons and request ++ * types that legitimately block (bulk I/O, syncs, blocking ++ * locks) keep stock behavior. ++ */ ++ switch (req->in.h.opcode) { ++ case FUSE_READ: ++ case FUSE_WRITE: ++ case FUSE_FLUSH: ++ case FUSE_FSYNC: ++ case FUSE_FSYNCDIR: ++ case FUSE_COPY_FILE_RANGE: ++ case FUSE_SETLKW: ++ break; ++ default: ++ if (fc->iq.ops != &fuse_dev_fiq_ops) { ++ u64 spin_until = ktime_get_ns() + 40000; ++ ++ while (!test_bit(FR_FINISHED, &req->flags) && ++ !need_resched() && ktime_get_ns() < spin_until) ++ cpu_relax(); ++ if (test_bit(FR_FINISHED, &req->flags)) ++ return; ++ } ++ break; ++ } ++ + if (!fc->no_interrupt) { + /* Any signal may interrupt this */ + err = wait_event_interruptible(req->waitq,