Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vvSvP-003JiC-0F for pgsql-hackers@arkaria.postgresql.org; Thu, 26 Feb 2026 04:24:31 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vvSvN-009dI5-0F for pgsql-hackers@arkaria.postgresql.org; Thu, 26 Feb 2026 04:24:29 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vvSvM-009dHv-2N for pgsql-hackers@lists.postgresql.org; Thu, 26 Feb 2026 04:24:28 +0000 Received: from mail-pl1-x632.google.com ([2607:f8b0:4864:20::632]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1vvSvI-00000001KR9-49i3 for pgsql-hackers@lists.postgresql.org; Thu, 26 Feb 2026 04:24:27 +0000 Received: by mail-pl1-x632.google.com with SMTP id d9443c01a7336-2a962230847so3563235ad.3 for ; Wed, 25 Feb 2026 20:24:25 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1772079863; x=1772684663; darn=lists.postgresql.org; h=to:references:message-id:content-transfer-encoding:cc:date :in-reply-to:from:subject:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=xcuC6OrCYu5+zqU54kE87/KW7Rvjsfa/vcD6KKZ7p8Q=; b=fIRbBmgL161VnGxnzXX40KKcGI5FumEWAMEFZtahQpFW3sJVYCMXI9NlXAX+h2+nIj dvXzEc0264Cc7d22EPFDu3MIvWTZUYnL67HJzmJvHXZFqgbQnM8UBqiBF+vflPWs/oJq VNWzBeV3YYd0iSGS09l+XWyrgD4yC9fbqJIJk3JfmyYQoxdC4VxBvE8eKbiKj/Y6b+Pl E48J2gq9z/MsChwtEU3x6H3/JfI5Ssut9T351XOutecus/v00fUFIz0OYKaQXyiXraUU 4xf9ijCg36YIckoTx0pnf2+PXex4RgG6g+0g/hXzJgnzWfLnRjmWPYbDzTR9h1oaWw+L q+3A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1772079863; x=1772684663; h=to:references:message-id:content-transfer-encoding:cc:date :in-reply-to:from:subject:mime-version:x-gm-gg:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=xcuC6OrCYu5+zqU54kE87/KW7Rvjsfa/vcD6KKZ7p8Q=; b=kmlTkr2yBBI2UDjF7eO9XsxZ4/rTFAgkeDVS66CJhj2s8a4ViMhwWsjGIi2CqqOsai xUZRNKcZC/EMOmhLgrWC0/ra4M74KL6CWo/NW50gVdmjPa4A43JfliBy89H9UrY6OvRz hMKWQK3ULXIAK1NTxyAOr5FCs8SvW7ihj2dwte9denk2GMYVqEuk3gVbFANOL84sr428 uiAPu3CdB+kJuZ9128zwxAdtJpE+C1dNXcEVEUI9hAb6OVYM2zhG4FrDuLC+ZNugk79p sAKsm4Hs1B22UP5Md0AgCXmyMJnGiu4wlq4cNzTDjBDMguSWX4+O6NLAlfnVyTHq29ZM AFSA== X-Forwarded-Encrypted: i=1; AJvYcCV34obBZO1wOsOklQIbtzh33eRO1mHaFw8y5uIsSOW2Rsem8+IF58YO7pG2NszZGQY95eX9FXTlACwtCqhG@lists.postgresql.org X-Gm-Message-State: AOJu0YwXZeS0AqBezl5GDBEzNf9BqJL/jk29Zpda9G7sNDfrMMQtAmps bHLJb2dTtkecDZkaKOxXncDhOcU+tRGgSqLXhmbd77obXiIFc8QIgMAthlml2g== X-Gm-Gg: ATEYQzwxrf0FYXA4kENMaVvpvtMa5qkEjpOdFMQ2D9zq7FCKowqABitqDpDkKu4Iafb 1bk154UtXTBgJoVOcUF1KYFk3X8x+LdO0kKluSsGcU8xlypWsW6AT+Q58T3ZA8XW9y9tyw7U2eO 7j7bze2Wamixb/PKEAGupabpD4lo6zSQdyUBd6Bg/IXOkNx0i4m4H48rY6meUsJy5D06wopKQ4R 6FLj1tDS78C6kRcZt2LAPY2290BLDr5tjlK5l3qqHiS63v3wtaFwg+bH0K8MvmuleTesEEYkGS6 +6VbCORoX+Ir4AUYc8HHwpTpe/yqNMdIwG5OuvPhUnWKWG67Rq2Vlqxgu1XTFnx4OO5jQjnYxy9 LnU6UW+IKr0Dcla5lphHtps9KgetOCHkcbtR1gwICXzErg4cuXe917+ugoHebW9VA70HG5eY9ke TIQ3C0JdsnJwwDiw96+ppy0mt7MBvzbw== X-Received: by 2002:a17:903:b8d:b0:2ab:24fa:f7a0 with SMTP id d9443c01a7336-2ae022afd64mr10078675ad.11.1772079863187; Wed, 25 Feb 2026 20:24:23 -0800 (PST) Received: from smtpclient.apple ([203.10.98.27]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2adfb5b2268sm8900815ad.6.2026.02.25.20.24.20 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 25 Feb 2026 20:24:22 -0800 (PST) Content-Type: text/plain; charset=utf-8 Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3864.400.21\)) Subject: Re: Checkpointer write combining From: Chao Li In-Reply-To: <377BC880-1616-4DEF-B9EF-5E297C358F7D@gmail.com> Date: Thu, 26 Feb 2026 12:23:45 +0800 Cc: Nazir Bilal Yavuz , PostgreSQL Hackers , Andres Freund Content-Transfer-Encoding: quoted-printable Message-Id: References: <2FA0BAC7-5413-4ABD-94CA-4398FE77750D@gmail.com> <3FC2442D-1012-4079-A009-A0B8C45E092D@gmail.com> <6B520FA8-B805-4C0D-B8B9-202B3AF168A9@gmail.com> <0198DBB9-4A76-49E4-87F8-43D46DD0FD76@gmail.com> <4ECED07F-75F2-46D4-8F71-0B0DFA350777@gmail.com> <20E76846-4816-45BC-84F0-D8836D9AFA4C@gmail.com> <0D990493-9DF4-4136-B1A1-CA0112E809ED@gmail.com> <377BC880-1616-4DEF-B9EF-5E297C358F7D@gmail.com> To: Melanie Plageman X-Mailer: Apple Mail (2.3864.400.21) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk > On Feb 25, 2026, at 14:52, Chao Li wrote: >=20 >=20 >=20 >> On Feb 20, 2026, at 07:41, Melanie Plageman = wrote: >>=20 >> On Wed, Jan 14, 2026 at 6:35=E2=80=AFPM Melanie Plageman >> wrote: >>>=20 >>> Thanks! v13 attached. >>=20 >> I've added background writer write combining and then spent some time >> refactoring and benchmarking this over the last few weeks, and I've >> realized I won't be able to get it in shape for Postgres 19. I've >> attached version 14 here with my WIP code so I can come back to it in >> the next release. >>=20 >> 0012 is an invasive refactor of GetVictimBuffer() and all the >> functions it calls that needs to be split apart into smaller commits >> and redistributed throughout the other patches in the set. Other than >> that, there are a number of todos both in the code and from a >> benchmarking perspective: >>=20 >> - Probe for preceding as well as following blocks when looking for >> blocks to combine >> - Experiment with different batch sizes >> - Enable and test bulkread and vacuum write combining >>=20 >> I also periodically get this troubling warning: "WARNING: invalid >> page in block 2 of relation "base/16384/2608_fsm"; zeroing out page". >> So clearly there's at least one bug somewhere. >>=20 >> FWIW benchmarks showed a large improvement from backend and bgwriter >> write combining, but I'll provide more detailed output from = benchmarks >> when I post a more reviewable patch set. >>=20 >> 0001 and 0002 are completely independent cleanup that should be = pushed >> anyway, so I might do that, but I didn't want to do so without having >> posted on the list at least once. >>=20 >> - Melanie >> = >=20 > A few comments for v14: >=20 > 1 - 0001 > ``` > - FlushBuffer(buf, reln, IOOBJECT_RELATION, IOCONTEXT_NORMAL); > + FlushBuffer(buf, reln, io_object, io_context); > ``` >=20 > This changes the hardcode IOOBJECT_RELATION to io_object when calling = FlushBuffer(). But FlushBuffer() itself still uses IOOBJECT_RELATION = instead of io_object, so the change will actually not take effective: > ``` > pgstat_count_io_op_time(IOOBJECT_RELATION, io_context, > IOOP_WRITE, io_start, 1, = BLCKSZ); > ``` >=20 > So, an update in FlushBuffer() is also needed. >=20 > 2 - 0003 - freelist.c > ``` > bool > StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, = bool from_ring) > { > + Assert(strategy); > + > /* We only do this in bulkread mode */ > if (strategy->btype !=3D BAS_BULKREAD) > return false; > @@ -795,8 +800,14 @@ StrategyRejectBuffer(BufferAccessStrategy = strategy, BufferDesc *buf, bool from_r > strategy->buffers[strategy->current] !=3D = BufferDescriptorGetBuffer(buf)) > return false; >=20 > + Assert(BufferIsLockedByMe(BufferDescriptorGetBuffer(buf))); > + Assert(!(pg_atomic_read_u64(&buf->state) & BM_LOCKED)); > ``` >=20 > I don=E2=80=99t quite understand = Assert(!(pg_atomic_read_u64(&buf->state) & BM_LOCKED)); >=20 > The comment says =E2=80=9Cthe buffer header spinlock must not be = held,=E2=80=9D but I doubt this Assert actually ensures that. Since = BM_LOCKED is just a bit in a shared state, isn't it possible for a = concurrent backend to grab the lock right when we=E2=80=99re checking = it? >=20 > If a concurrent process locks the header a split-second before the = Assert, we=E2=80=99ll get a false-positive crash in a dev environment = even though the code is fine. If they lock it a split-second after, then = the Assert didn't really catch anything. It feels like a race condition = either way. >=20 > I think the only way to truly ensure a spinlock is "not held" is to = actually acquire it. If we=E2=80=99re just checking the bit like this, = it=E2=80=99s just a snapshot that might be stale by the time the = instruction finishes. What was the specific worry here=E2=80=94a = self-deadlock, or something else? >=20 > 3 - 0004 > ``` > +typedef enum BufferUsageCountChange > +{ > + BUC_ZERO, > + BUC_MAX_ONE, > + BUC_ONE, > +} BufferUsageCountChange; > ``` >=20 > Can we add some brief comments to explain every enum item? I feel hard = to guess the meaning from the names without further reading the code. >=20 > 4 - 0005 - bufmgr.c > ``` > +/* > + * Prepare bufdesc for eager flushing. > + * > + * Given bufnum, return the buffer descriptor of the buffer to = eagerly flush, > + * pinned and locked and with BM_IO_IN_PROGRESS set, or NULL if this = buffer > + * does not contain a block that should be flushed. > + * > + * If returning a buffer, also return its LSN. > + */ > +static BufferDesc * > +PrepareOrRejectEagerFlushBuffer(Buffer bufnum) > ``` >=20 > This header comment looks stale. >=20 > * It says =E2=80=9Cand with BM_IO_IN_PROGRESS set=E2=80=9D, but I = don=E2=80=99t see where BM_IO_IN_PROGRESS is set in this function. > * It says =E2=80=9Calso return its LSN=E2=80=9D, but I don=E2=80=99t = see LSN is returned. >=20 > 5 - 0006 > ``` > + /* Start IO on the first buffer */ > + if (!StartBufferIO(buf_hdr, false, false)) > + goto clean; > ``` >=20 > This failure branch feels like leaking the content lock. The buffer = was already share-locked via BufferLockConditional() earlier, and I = don=E2=80=99t see the =E2=80=9Cclean" path unlock that content lock = before returning. >=20 > 6 - 0009 > ``` > + FlushUnlockedBuffer(bufHdr, NULL, IOOBJECT_RELATION, = IOCONTEXT_NORMAL); > + UnpinBuffer(bufHdr); > + > + ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, = &bufHdr->tag); > ``` >=20 > bufHdr is unlocked and unpinned, it might be unsafe to still use = bufHdr->tag. I think we can copy bufHdr->tag to a local variable before = unlock the buffer. >=20 > ~~ My brain is stuck, I will continue 0010 tomorrow ~~ >=20 7 - 0010 ``` - * batch_limit is the largest batch we are allowed to construct given = the - * remaining blocks in the table, the number of available pins, and the - * current configuration. + * max_batch_size is the maximum number of blocks that can be combined = into a + * single write in general. This function, based on the block number of = start, + * will determine the maximum IO size for this particular write given = how much + * of the file remains. max_batch_size is provided by the caller so it = doesn't + * have to be recalculated for each write. ``` I don=E2=80=99t see the function FindStrategyFlushAdjacents() has a = parameter named max_batch_size, but batch_limit is still there. 8 - 0010 ``` static void +FindFlushAdjacents(BufferDesc *batch_start, + uint32 batch_limit, + BufferWriteBatch *batch) +{ + BufferTag require; /* requested block's = buffer tag */ + uint32 newHash; /* hash value for = require */ + LWLock *newPartitionLock; /* buffer partition lock for it = */ + int buf_id; + + /* create a tag so we can lookup the buffers */ + InitBufferTag(&require, &batch->reln->smgr_rlocator.locator, + batch->forkno, InvalidBlockNumber); + + for (; batch->n < batch_limit; batch->n++) + { + XLogRecPtr lsn; + + require.blockNum =3D batch->start + batch->n; + + Assert(BlockNumberIsValid(require.blockNum)); ``` When I first time read the Assert, I was confused how it can assume the = block to be valid, then I realized that WriteBatchInit() ensures a safe = =E2=80=9Climit=E2=80=9D. Maybe add a brief comment for the Assert. 9 - 0010 ``` + batch->bufdescs[batch->n] =3D + PrepareOrRejectEagerFlushBuffer(buf_id + 1, + = &require, + = newPartitionLock, + = &lsn); + if (lsn > batch->max_lsn) + batch->max_lsn =3D lsn; + + if (batch->bufdescs[batch->n] =3D=3D NULL) + break; ``` I think we can move if (batch->bufdescs[batch->n] =3D=3D NULL) to before = if (lsn > batch->max_lsn). This is a correctness issue, because PrepareOrRejectEagerFlushBuffer = will set lsn to InvalidXLogRecPtr when returns NULL, but doing the NULL = check early just feels more reasonable. 10 - 0010 ``` + * max_lsn may be updated if the provided buffer LSN exceeds the = current max + * LSN. */ static BufferDesc * PrepareOrRejectEagerFlushBuffer(Buffer bufnum, = BufferTag *require, + LWLock = *buftable_lock, = XLogRecPtr *lsn) ``` The function doesn=E2=80=99t have a parameter named max_lsn, is it = =E2=80=9Clsn=E2=80=9D? 11 - 0010 ``` +FindFlushAdjacents(BufferDesc *batch_start, + uint32 batch_limit, + BufferWriteBatch *batch) ``` Looks like batch_start is not used at all in this function. 12 - 0011 ``` + * Callers specify if and by how much they want to bump the buffer's = usage + * count. ``` I don=E2=80=99t get what this comment means. ~~ As 0012 is WIP, I didn't review it. ~~ Best regards, -- Chao Li (Evan) HighGo Software Co., Ltd. https://www.highgo.com/