Index | Thread | Search

From:
selsta <selsta@sent.at>
Subject:
uvn_flush: walk resident pages
To:
tech@openbsd.org
Date:
Sun, 18 Oct 2026 20:04:51 +0200

Download raw body.

Thread
  • selsta:

    uvn_flush: walk resident pages

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