From 1096af12774bb26b74cd2e943438e33a1ce24b19 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 17:42:33 +0800 Subject: [PATCH 1/2] perf(fuse): bounded spin-wait for metadata request completion (CORE-48) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On virtualized ARM64 every reschedule IPI is a trap (no TIF_POLLING_NRFLAG, single LLC domain keeps wake_affine from migrating the waiter onto the IRQ CPU), so a synchronous FUSE workload pays device-IRQ trap + IPI trap + two context switches per operation — the reschedule-IPI count matches the virtio-fs request IRQ count 1:1. Poll FR_FINISHED for up to 40us before sleeping (KVM halt polling at the FUSE wait point), gated to non-READ/WRITE opcodes: bulk replies outlive the budget and an ungated spin cost -54% sequential-read throughput. A/B on the ArcBox bench trio (Apple VZ guest, 18 vCPU): metadata_stat 31.8k -> 50.3k ops/s (+58%, 97% of the hard-pinned ceiling), negative_lookup +40%, create_delete +28%, sequential read -3.5% (noise). Beats Colima's same-context numbers. Full investigation record: Linear CORE-48. --- patches/fuse-spin-wait.patch | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 patches/fuse-spin-wait.patch diff --git a/patches/fuse-spin-wait.patch b/patches/fuse-spin-wait.patch new file mode 100644 index 0000000..2ba1762 --- /dev/null +++ b/patches/fuse-spin-wait.patch @@ -0,0 +1,60 @@ +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. + +READ/WRITE requests are excluded: their replies outlive any sane spin +budget, and burning the budget anyway cost -54% sequential-read +throughput in A/B testing. With the gate 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,24 @@ static void request_wait_answer(struct fuse_req *req) + struct fuse_iqueue *fiq = &fc->iq; + int err; + ++ /* ++ * Poll briefly before sleeping: 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. Bulk READ/WRITE replies ++ * outlive any sane budget and regressed -54% under the burnt ++ * CPU, so only metadata-class requests spin. ++ */ ++ if (req->in.h.opcode != FUSE_READ && req->in.h.opcode != FUSE_WRITE) { ++ 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; ++ } ++ + if (!fc->no_interrupt) { + /* Any signal may interrupt this */ + err = wait_event_interruptible(req->waitq, From eed4dfdf64e6cbc61e2239539afc4f28b81513fb Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 20:15:48 +0800 Subject: [PATCH 2/2] perf(fuse): gate the spin to virtio-class transports and non-blocking opcodes Review round: the single READ/WRITE exclusion left slow non-metadata requests (flush/fsync, copy_file_range, blocking locks) burning the budget, and classic /dev/fuse daemons with arbitrary completion latency took the spin path on every build. The spin now requires a kernel-mediated transport (fc->iq.ops != fuse_dev_fiq_ops, i.e. virtio-fs) and skips every legitimately-blocking opcode. Revalidated on hardware: metadata_stat 50.6k (+57% vs stock), negative_lookup 37.6k (+57%), create_delete +17%, find_recursive +11%, rm_rf 14% faster, sequential read -3% (noise). --- patches/fuse-spin-wait.patch | 52 ++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/patches/fuse-spin-wait.patch b/patches/fuse-spin-wait.patch index 2ba1762..b44bd6c 100644 --- a/patches/fuse-spin-wait.patch +++ b/patches/fuse-spin-wait.patch @@ -14,9 +14,14 @@ 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. -READ/WRITE requests are excluded: their replies outlive any sane spin -budget, and burning the budget anyway cost -54% sequential-read -throughput in A/B testing. With the gate the win is one-sided +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) @@ -33,26 +38,39 @@ is the natural upstream refinement. #include +#include #include -@@ -547,6 +548,24 @@ static void request_wait_answer(struct fuse_req *req) +@@ -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: 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. Bulk READ/WRITE replies -+ * outlive any sane budget and regressed -54% under the burnt -+ * CPU, so only metadata-class requests spin. ++ * 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. + */ -+ if (req->in.h.opcode != FUSE_READ && req->in.h.opcode != FUSE_WRITE) { -+ u64 spin_until = ktime_get_ns() + 40000; ++ 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; ++ 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) {