From: selsta Subject: uvn_flush: walk resident pages To: tech@openbsd.org Date: Sun, 18 Oct 2026 20:04:51 +0200 uvn_flush() looks up every page-sized offset in the requested range, twice for PGO_CLEANIT. Large msync(MS_SYNC) calls can therefore be expensive even when few pages are resident and none are dirty. Walk the resident pages instead. The skipped lookups run under the object write lock, so skipping them cannot miss concurrent insertions. The PG_CLEANCHK pass keeps that lock throughout and can use RBT_NEXT. The flush pass re-finds by offset because it may free pages or drop the lock. Set curoff to the selected offset to preserve the busy-page retry. Tested on 7.9/arm64 (GENERIC.MP, 4 vCPUs, 8 GiB RAM) with a synthetic LMDB MDB_WRITEMAP database containing 32 GiB of payload and 2 GiB of distributed pages warmed. The second warming pass had no faults. Swap use was zero. Elapsed ms, medians of three runs, same binary and database: stock patched stock again clean MS_SYNC 749.0 43.6 768.7 small synchronous txn 757.0 46.9 777.3 Both sync the full used extent (~32 GiB). The transaction replaces one 8-byte value and includes begin, put and commit. All payload bytes validated before and after the patched runs. Found while investigating: https://github.com/monero-project/monero/issues/7027 Diff against uvm_vnode.c r1.152; tested on 7.9's r1.151, where uvn_flush() is identical. I used Codex for the investigation, patch, testing and wording of this email. selsta --- sys/uvm/uvm_vnode.c +++ sys/uvm/uvm_vnode.c @@ -584,7 +584,7 @@ uvn_flush(struct uvm_object *uobj, voff_t start, voff_t stop, int flags) { struct uvm_vnode *uvn = (struct uvm_vnode *) uobj; - struct vm_page *pp, *ptmp; + struct vm_page key, *pp, *ptmp; struct vm_page *pps[MAXBSIZE >> PAGE_SHIFT], **ppsp; int npages, result, lcv; boolean_t retval, need_iosync, needs_clean; @@ -615,10 +615,14 @@ */ if ((flags & PGO_CLEANIT) != 0) { KASSERT(uobj->pgops->pgo_mk_pcluster != 0); - for (curoff = start ; curoff < stop; curoff += PAGE_SIZE) { - if ((pp = uvm_pagelookup(uobj, curoff)) != NULL) - atomic_clearbits_int(&pp->pg_flags, - PG_CLEANCHK); + key.offset = start; + for (pp = RBT_NFIND(uvm_objtree, &uobj->memt, &key); + pp != NULL && pp->offset < stop; + pp = RBT_NEXT(uvm_objtree, pp)) { + KASSERT(uobj->uo_npages != 0); + KASSERT((pp->pg_flags & PG_RELEASED) == 0 || + (pp->pg_flags & PG_BUSY) != 0); + atomic_clearbits_int(&pp->pg_flags, PG_CLEANCHK); } } @@ -626,8 +630,15 @@ uvm_lock_pageq(); /* locked: both page queues */ for (curoff = start; curoff < stop; curoff += PAGE_SIZE) { - if ((pp = uvm_pagelookup(uobj, curoff)) == NULL) - continue; + /* Re-find by offset: flushing may free pages or unlock uobj. */ + key.offset = curoff; + pp = RBT_NFIND(uvm_objtree, &uobj->memt, &key); + if (pp == NULL || pp->offset >= stop) + break; + KASSERT(uobj->uo_npages != 0); + KASSERT((pp->pg_flags & PG_RELEASED) == 0 || + (pp->pg_flags & PG_BUSY) != 0); + curoff = pp->offset; /* * handle case where we do not need to clean page (either * because we are not clean or because page is not dirty or