agora inbox for [email protected]
help / color / mirror / Atom feed[PATCH v25 5/9] Row pattern recognition patch (executor).
225+ messages / 4 participants
[nested] [flat]
* [PATCH v25 5/9] Row pattern recognition patch (executor).
@ 2024-12-21 06:19 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Tatsuo Ishii @ 2024-12-21 06:19 UTC (permalink / raw)
---
src/backend/executor/nodeWindowAgg.c | 1745 +++++++++++++++++++++++++-
src/backend/utils/adt/windowfuncs.c | 37 +-
src/include/catalog/pg_proc.dat | 6 +
src/include/nodes/execnodes.h | 30 +
4 files changed, 1807 insertions(+), 11 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 70a7025818..2148d4ed1e 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -36,6 +36,7 @@
#include "access/htup_details.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_aggregate.h"
+#include "catalog/pg_collation_d.h"
#include "catalog/pg_proc.h"
#include "executor/executor.h"
#include "executor/nodeWindowAgg.h"
@@ -45,9 +46,11 @@
#include "optimizer/optimizer.h"
#include "parser/parse_agg.h"
#include "parser/parse_coerce.h"
+#include "regex/regex.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
+#include "utils/fmgroids.h"
#include "utils/expandeddatum.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@@ -159,6 +162,58 @@ typedef struct WindowStatePerAggData
bool restart; /* need to restart this agg in this cycle? */
} WindowStatePerAggData;
+/*
+ * Set of StringInfo. Used in RPR.
+ */
+typedef struct StringSet
+{
+ StringInfo *str_set;
+ Size set_size; /* current array allocation size in number of
+ * items */
+ int set_index; /* current used size */
+} StringSet;
+
+/*
+ * Allowed subsequent PATTERN variables positions.
+ * Used in RPR.
+ *
+ * pos represents the pattern variable defined order in DEFINE caluase. For
+ * example. "DEFINE START..., UP..., DOWN ..." and "PATTERN START UP DOWN UP"
+ * will create:
+ * VariablePos[0].pos[0] = 0; START
+ * VariablePos[1].pos[0] = 1; UP
+ * VariablePos[1].pos[1] = 3; UP
+ * VariablePos[2].pos[0] = 2; DOWN
+ *
+ * Note that UP has two pos because UP appears in PATTERN twice.
+ *
+ * By using this strucrture, we can know which pattern variable can be followed
+ * by which pattern variable(s). For example, START can be followed by UP and
+ * DOWN since START's pos is 0, and UP's pos is 1 or 3, DOWN's pos is 2.
+ * DOWN can be followed by UP since UP's pos is either 1 or 3.
+ *
+ */
+
+/*
+ * Structure used by check_rpr_navigation() and rpr_navigation_walker().
+ */
+typedef struct NavigationInfo
+{
+ bool is_prev; /* true if PREV */
+ int num_vars; /* number of var nodes */
+} NavigationInfo;
+
+#define NUM_ALPHABETS 26 /* we allow [a-z] variable initials */
+typedef struct VariablePos
+{
+ int pos[NUM_ALPHABETS]; /* postion(s) in PATTERN */
+} VariablePos;
+
+/*
+ * Regular expression compiled cache for do_pattern_match exists
+ */
+static regex_t *regcache = NULL;
+
static void initialize_windowaggregate(WindowAggState *winstate,
WindowStatePerFunc perfuncstate,
WindowStatePerAgg peraggstate);
@@ -184,6 +239,7 @@ static void release_partition(WindowAggState *winstate);
static int row_is_in_frame(WindowAggState *winstate, int64 pos,
TupleTableSlot *slot);
+
static void update_frameheadpos(WindowAggState *winstate);
static void update_frametailpos(WindowAggState *winstate);
static void update_grouptailpos(WindowAggState *winstate);
@@ -195,9 +251,52 @@ static Datum GetAggInitVal(Datum textInitVal, Oid transtype);
static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1,
TupleTableSlot *slot2);
+
+static int WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+ int relpos, int seektype, bool set_mark,
+ bool *isnull, bool *isout);
static bool window_gettupleslot(WindowObject winobj, int64 pos,
TupleTableSlot *slot);
+static void attno_map(Node *node);
+static bool attno_map_walker(Node *node, void *context);
+static int row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+static bool rpr_is_defined(WindowAggState *winstate);
+
+static void create_reduced_frame_map(WindowAggState *winstate);
+static int get_reduced_frame_map(WindowAggState *winstate, int64 pos);
+static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
+ int val);
+static void clear_reduced_frame_map(WindowAggState *winstate);
+static void update_reduced_frame(WindowObject winobj, int64 pos);
+
+static int64 evaluate_pattern(WindowObject winobj, int64 current_pos,
+ char *vname, StringInfo encoded_str, bool *result);
+
+static bool get_slots(WindowObject winobj, int64 current_pos);
+
+static int search_str_set(char *pattern, StringSet *str_set,
+ VariablePos *variable_pos);
+static char pattern_initial(WindowAggState *winstate, char *vname);
+static int do_pattern_match(char *pattern, char *encoded_str, int len);
+static void do_pattern_match_finish(void);
+
+static StringSet *string_set_init(void);
+static void string_set_add(StringSet *string_set, StringInfo str);
+static StringInfo string_set_get(StringSet *string_set, int index);
+static int string_set_get_size(StringSet *string_set);
+static void string_set_discard(StringSet *string_set);
+static VariablePos *variable_pos_init(void);
+static void variable_pos_register(VariablePos *variable_pos, char initial,
+ int pos);
+static bool variable_pos_compare(VariablePos *variable_pos,
+ char initial1, char initial2);
+static int variable_pos_fetch(VariablePos *variable_pos, char initial,
+ int index);
+static void variable_pos_discard(VariablePos *variable_pos);
+
+static void check_rpr_navigation(Node *node, bool is_prev);
+static bool rpr_navigation_walker(Node *node, void *context);
/*
* initialize_windowaggregate
@@ -774,10 +873,12 @@ eval_windowaggregates(WindowAggState *winstate)
* transition function, or
* - we have an EXCLUSION clause, or
* - if the new frame doesn't overlap the old one
+ * - if RPR is enabled
*
* Note that we don't strictly need to restart in the last case, but if
* we're going to remove all rows from the aggregation anyway, a restart
* surely is faster.
+ * we restart aggregation too.
*----------
*/
numaggs_restart = 0;
@@ -788,7 +889,8 @@ eval_windowaggregates(WindowAggState *winstate)
(winstate->aggregatedbase != winstate->frameheadpos &&
!OidIsValid(peraggstate->invtransfn_oid)) ||
(winstate->frameOptions & FRAMEOPTION_EXCLUSION) ||
- winstate->aggregatedupto <= winstate->frameheadpos)
+ winstate->aggregatedupto <= winstate->frameheadpos ||
+ rpr_is_defined(winstate))
{
peraggstate->restart = true;
numaggs_restart++;
@@ -862,7 +964,22 @@ eval_windowaggregates(WindowAggState *winstate)
* head, so that tuplestore can discard unnecessary rows.
*/
if (agg_winobj->markptr >= 0)
- WinSetMarkPosition(agg_winobj, winstate->frameheadpos);
+ {
+ int64 markpos = winstate->frameheadpos;
+
+ if (rpr_is_defined(winstate))
+ {
+ /*
+ * If RPR is used, it is possible PREV wants to look at the
+ * previous row. So the mark pos should be frameheadpos - 1
+ * unless it is below 0.
+ */
+ markpos -= 1;
+ if (markpos < 0)
+ markpos = 0;
+ }
+ WinSetMarkPosition(agg_winobj, markpos);
+ }
/*
* Now restart the aggregates that require it.
@@ -917,6 +1034,14 @@ eval_windowaggregates(WindowAggState *winstate)
{
winstate->aggregatedupto = winstate->frameheadpos;
ExecClearTuple(agg_row_slot);
+
+ /*
+ * If RPR is defined, we do not use aggregatedupto_nonrestarted. To
+ * avoid assertion failure below, we reset aggregatedupto_nonrestarted
+ * to frameheadpos.
+ */
+ if (rpr_is_defined(winstate))
+ aggregatedupto_nonrestarted = winstate->frameheadpos;
}
/*
@@ -930,6 +1055,12 @@ eval_windowaggregates(WindowAggState *winstate)
{
int ret;
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT,
+ winstate->aggregatedupto,
+ winstate->aggregatedbase);
+#endif
+
/* Fetch next row if we didn't already */
if (TupIsNull(agg_row_slot))
{
@@ -945,9 +1076,53 @@ eval_windowaggregates(WindowAggState *winstate)
ret = row_is_in_frame(winstate, winstate->aggregatedupto, agg_row_slot);
if (ret < 0)
break;
+
if (ret == 0)
goto next_tuple;
+ if (rpr_is_defined(winstate))
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "reduced_frame_map: %d aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT,
+ get_reduced_frame_map(winstate,
+ winstate->aggregatedupto),
+ winstate->aggregatedupto,
+ winstate->aggregatedbase);
+#endif
+
+ /*
+ * If the row status at currentpos is already decided and current
+ * row status is not decided yet, it means we passed the last
+ * reduced frame. Time to break the loop.
+ */
+ if (get_reduced_frame_map(winstate,
+ winstate->currentpos) != RF_NOT_DETERMINED &&
+ get_reduced_frame_map(winstate,
+ winstate->aggregatedupto) == RF_NOT_DETERMINED)
+ break;
+
+ /*
+ * Otherwise we need to calculate the reduced frame.
+ */
+ ret = row_is_in_reduced_frame(winstate->agg_winobj,
+ winstate->aggregatedupto);
+ if (ret == -1) /* unmatched row */
+ break;
+
+ /*
+ * Check if current row needs to be skipped due to no match.
+ */
+ if (get_reduced_frame_map(winstate,
+ winstate->aggregatedupto) == RF_SKIPPED &&
+ winstate->aggregatedupto == winstate->aggregatedbase)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "skip current row for aggregation");
+#endif
+ break;
+ }
+ }
+
/* Set tuple context for evaluation of aggregate arguments */
winstate->tmpcontext->ecxt_outertuple = agg_row_slot;
@@ -976,6 +1151,7 @@ next_tuple:
ExecClearTuple(agg_row_slot);
}
+
/* The frame's end is not supposed to move backwards, ever */
Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
@@ -1199,6 +1375,7 @@ begin_partition(WindowAggState *winstate)
winstate->framehead_valid = false;
winstate->frametail_valid = false;
winstate->grouptail_valid = false;
+ create_reduced_frame_map(winstate);
winstate->spooled_rows = 0;
winstate->currentpos = 0;
winstate->frameheadpos = 0;
@@ -2170,6 +2347,11 @@ ExecWindowAgg(PlanState *pstate)
CHECK_FOR_INTERRUPTS();
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "ExecWindowAgg called. pos: " INT64_FORMAT,
+ winstate->currentpos);
+#endif
+
if (winstate->status == WINDOWAGG_DONE)
return NULL;
@@ -2278,6 +2460,17 @@ ExecWindowAgg(PlanState *pstate)
/* don't evaluate the window functions when we're in pass-through mode */
if (winstate->status == WINDOWAGG_RUN)
{
+ /*
+ * If RPR is defined and skip mode is next row, we need to clear
+ * existing reduced frame info so that we newly calculate the info
+ * starting from current row.
+ */
+ if (rpr_is_defined(winstate))
+ {
+ if (winstate->rpSkipTo == ST_NEXT_ROW)
+ clear_reduced_frame_map(winstate);
+ }
+
/*
* Evaluate true window functions
*/
@@ -2444,6 +2637,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
TupleDesc scanDesc;
ListCell *l;
+ TargetEntry *te;
+ Expr *expr;
+
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -2542,6 +2738,16 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc,
&TTSOpsMinimalTuple);
+ winstate->prev_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+
+ winstate->next_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+
+ winstate->null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+ winstate->null_slot = ExecStoreAllNullTuple(winstate->null_slot);
+
/*
* create frame head and tail slots only if needed (must create slots in
* exactly the same cases that update_frameheadpos and update_frametailpos
@@ -2723,6 +2929,43 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->inRangeAsc = node->inRangeAsc;
winstate->inRangeNullsFirst = node->inRangeNullsFirst;
+ /* Set up SKIP TO type */
+ winstate->rpSkipTo = node->rpSkipTo;
+ /* Set up row pattern recognition PATTERN clause */
+ winstate->patternVariableList = node->patternVariable;
+ winstate->patternRegexpList = node->patternRegexp;
+
+ /* Set up row pattern recognition DEFINE clause */
+ winstate->defineInitial = node->defineInitial;
+ winstate->defineVariableList = NIL;
+ winstate->defineClauseList = NIL;
+ if (node->defineClause != NIL)
+ {
+ /*
+ * Tweak arg var of PREV/NEXT so that it refers to scan/inner slot.
+ */
+ foreach(l, node->defineClause)
+ {
+ char *name;
+ ExprState *exps;
+
+ te = lfirst(l);
+ name = te->resname;
+ expr = te->expr;
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "defineVariable name: %s", name);
+#endif
+ winstate->defineVariableList =
+ lappend(winstate->defineVariableList,
+ makeString(pstrdup(name)));
+ attno_map((Node *) expr);
+ exps = ExecInitExpr(expr, (PlanState *) winstate);
+ winstate->defineClauseList =
+ lappend(winstate->defineClauseList, exps);
+ }
+ }
+
winstate->all_first = true;
winstate->partition_spooled = false;
winstate->more_partitions = false;
@@ -2731,6 +2974,111 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
return winstate;
}
+/*
+ * Rewrite varno of Var nodes that are the argument of PREV/NET so that they
+ * see scan tuple (PREV) or inner tuple (NEXT). Also we check the arguments
+ * of PREV/NEXT include at least 1 column reference. This is required by the
+ * SQL standard.
+ */
+static void
+attno_map(Node *node)
+{
+ (void) expression_tree_walker(node, attno_map_walker, NULL);
+}
+
+static bool
+attno_map_walker(Node *node, void *context)
+{
+ FuncExpr *func;
+ int nargs;
+ bool is_prev;
+
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, FuncExpr))
+ {
+ func = (FuncExpr *) node;
+
+ if (func->funcid == F_PREV || func->funcid == F_NEXT)
+ {
+ /*
+ * The SQL standard allows to have two more arguments form of
+ * PREV/NEXT. But currently we allow only 1 argument form.
+ */
+ nargs = list_length(func->args);
+ if (list_length(func->args) != 1)
+ elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args",
+ func->funcid, nargs);
+
+ /*
+ * Check expr of PREV/NEXT aruguments and replace varno.
+ */
+ is_prev = (func->funcid == F_PREV) ? true : false;
+ check_rpr_navigation(node, is_prev);
+ }
+ }
+ return expression_tree_walker(node, attno_map_walker, NULL);
+}
+
+/*
+ * Rewrite varno of Var of RPR navigation operations (PREV/NEXT).
+ * If is_prev is true, we take care PREV, otherwise NEXT.
+ */
+static void
+check_rpr_navigation(Node *node, bool is_prev)
+{
+ NavigationInfo context;
+
+ context.is_prev = is_prev;
+ context.num_vars = 0;
+ (void) expression_tree_walker(node, rpr_navigation_walker, &context);
+ if (context.num_vars < 1)
+ ereport(ERROR,
+ errmsg("row pattern navigation operation's argument must include at least one column reference"));
+}
+
+static bool
+rpr_navigation_walker(Node *node, void *context)
+{
+ NavigationInfo *nav = (NavigationInfo *) context;
+
+ if (node == NULL)
+ return false;
+
+ switch (nodeTag(node))
+ {
+ case T_Var:
+ {
+ Var *var = (Var *) node;
+
+ nav->num_vars++;
+
+ if (nav->is_prev)
+ {
+ /*
+ * Rewrite varno from OUTER_VAR to regular var no so that
+ * the var references scan tuple.
+ */
+ var->varno = var->varnosyn;
+ }
+ else
+ var->varno = INNER_VAR;
+ }
+ break;
+ case T_Const:
+ case T_FuncExpr:
+ case T_OpExpr:
+ break;
+
+ default:
+ ereport(ERROR,
+ errmsg("row pattern navigation operation's argument includes unsupported expression"));
+ }
+ return expression_tree_walker(node, rpr_navigation_walker, context);
+}
+
+
/* -----------------
* ExecEndWindowAgg
* -----------------
@@ -2788,6 +3136,8 @@ ExecReScanWindowAgg(WindowAggState *node)
ExecClearTuple(node->agg_row_slot);
ExecClearTuple(node->temp_slot_1);
ExecClearTuple(node->temp_slot_2);
+ ExecClearTuple(node->prev_slot);
+ ExecClearTuple(node->next_slot);
if (node->framehead_slot)
ExecClearTuple(node->framehead_slot);
if (node->frametail_slot)
@@ -3148,7 +3498,8 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot)
return false;
if (pos < winobj->markpos)
- elog(ERROR, "cannot fetch row before WindowObject's mark position");
+ elog(ERROR, "cannot fetch row: " INT64_FORMAT " before WindowObject's mark position: " INT64_FORMAT,
+ pos, winobj->markpos);
oldcontext = MemoryContextSwitchTo(winstate->ss.ps.ps_ExprContext->ecxt_per_query_memory);
@@ -3468,14 +3819,54 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
WindowAggState *winstate;
ExprContext *econtext;
TupleTableSlot *slot;
- int64 abs_pos;
- int64 mark_pos;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
econtext = winstate->ss.ps.ps_ExprContext;
slot = winstate->temp_slot_1;
+ if (WinGetSlotInFrame(winobj, slot,
+ relpos, seektype, set_mark,
+ isnull, isout) == 0)
+ {
+ econtext->ecxt_outertuple = slot;
+ return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
+ econtext, isnull);
+ }
+
+ if (isout)
+ *isout = true;
+ *isnull = true;
+ return (Datum) 0;
+}
+
+/*
+ * WinGetSlotInFrame
+ * slot: TupleTableSlot to store the result
+ * relpos: signed rowcount offset from the seek position
+ * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
+ * set_mark: If the row is found/in frame and set_mark is true, the mark is
+ * moved to the row as a side-effect.
+ * isnull: output argument, receives isnull status of result
+ * isout: output argument, set to indicate whether target row position
+ * is out of frame (can pass NULL if caller doesn't care about this)
+ *
+ * Returns 0 if we successfullt got the slot. false if out of frame.
+ * (also isout is set)
+ */
+static int
+WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+ int relpos, int seektype, bool set_mark,
+ bool *isnull, bool *isout)
+{
+ WindowAggState *winstate;
+ int64 abs_pos;
+ int64 mark_pos;
+ int num_reduced_frame;
+
+ Assert(WindowObjectIsValid(winobj));
+ winstate = winobj->winstate;
+
switch (seektype)
{
case WINDOW_SEEK_CURRENT:
@@ -3542,11 +3933,25 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
winstate->frameOptions);
break;
}
+ num_reduced_frame = row_is_in_reduced_frame(winobj,
+ winstate->frameheadpos);
+ if (num_reduced_frame < 0)
+ goto out_of_frame;
+ else if (num_reduced_frame > 0)
+ if (relpos >= num_reduced_frame)
+ goto out_of_frame;
break;
case WINDOW_SEEK_TAIL:
/* rejecting relpos > 0 is easy and simplifies code below */
if (relpos > 0)
goto out_of_frame;
+
+ /*
+ * RPR cares about frame head pos. Need to call
+ * update_frameheadpos
+ */
+ update_frameheadpos(winstate);
+
update_frametailpos(winstate);
abs_pos = winstate->frametailpos - 1 + relpos;
@@ -3613,6 +4018,14 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
mark_pos = 0; /* keep compiler quiet */
break;
}
+
+ num_reduced_frame = row_is_in_reduced_frame(winobj,
+ winstate->frameheadpos + relpos);
+ if (num_reduced_frame < 0)
+ goto out_of_frame;
+ else if (num_reduced_frame > 0)
+ abs_pos = winstate->frameheadpos + relpos +
+ num_reduced_frame - 1;
break;
default:
elog(ERROR, "unrecognized window seek type: %d", seektype);
@@ -3631,15 +4044,13 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
*isout = false;
if (set_mark)
WinSetMarkPosition(winobj, mark_pos);
- econtext->ecxt_outertuple = slot;
- return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
- econtext, isnull);
+ return 0;
out_of_frame:
if (isout)
*isout = true;
*isnull = true;
- return (Datum) 0;
+ return -1;
}
/*
@@ -3670,3 +4081,1319 @@ WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull)
return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
econtext, isnull);
}
+
+/*
+ * rpr_is_defined
+ * return true if Row pattern recognition is defined.
+ */
+static
+bool
+rpr_is_defined(WindowAggState *winstate)
+{
+ return winstate->patternVariableList != NIL;
+}
+
+/*
+ * -----------------
+ * row_is_in_reduced_frame
+ * Determine whether a row is in the current row's reduced window frame
+ * according to row pattern matching
+ *
+ * The row must has been already determined that it is in a full window frame
+ * and fetched it into slot.
+ *
+ * Returns:
+ * = 0, RPR is not defined.
+ * >0, if the row is the first in the reduced frame. Return the number of rows
+ * in the reduced frame.
+ * -1, if the row is unmatched row
+ * -2, if the row is in the reduced frame but needed to be skipped because of
+ * AFTER MATCH SKIP PAST LAST ROW
+ * -----------------
+ */
+static
+int
+row_is_in_reduced_frame(WindowObject winobj, int64 pos)
+{
+ WindowAggState *winstate = winobj->winstate;
+ int state;
+ int rtn;
+
+ if (!rpr_is_defined(winstate))
+ {
+ /*
+ * RPR is not defined. Assume that we are always in the the reduced
+ * window frame.
+ */
+ rtn = 0;
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT,
+ rtn, pos);
+#endif
+ return rtn;
+ }
+
+ state = get_reduced_frame_map(winstate, pos);
+
+ if (state == RF_NOT_DETERMINED)
+ {
+ update_frameheadpos(winstate);
+ update_reduced_frame(winobj, pos);
+ }
+
+ state = get_reduced_frame_map(winstate, pos);
+
+ switch (state)
+ {
+ int64 i;
+ int num_reduced_rows;
+
+ case RF_FRAME_HEAD:
+ num_reduced_rows = 1;
+ for (i = pos + 1;
+ get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
+ num_reduced_rows++;
+ rtn = num_reduced_rows;
+ break;
+
+ case RF_SKIPPED:
+ rtn = -2;
+ break;
+
+ case RF_UNMATCHED:
+ rtn = -1;
+ break;
+
+ default:
+ elog(ERROR, "Unrecognized state: %d at: " INT64_FORMAT,
+ state, pos);
+ break;
+ }
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT,
+ rtn, pos);
+#endif
+ return rtn;
+}
+
+#define REDUCED_FRAME_MAP_INIT_SIZE 1024L
+
+/*
+ * create_reduced_frame_map
+ * Create reduced frame map
+ */
+static
+void
+create_reduced_frame_map(WindowAggState *winstate)
+{
+ winstate->reduced_frame_map =
+ MemoryContextAlloc(winstate->partcontext,
+ REDUCED_FRAME_MAP_INIT_SIZE);
+ winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
+ clear_reduced_frame_map(winstate);
+}
+
+/*
+ * clear_reduced_frame_map
+ * Clear reduced frame map
+ */
+static
+void
+clear_reduced_frame_map(WindowAggState *winstate)
+{
+ Assert(winstate->reduced_frame_map != NULL);
+ MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
+ winstate->alloc_sz);
+}
+
+/*
+ * get_reduced_frame_map
+ * Get reduced frame map specified by pos
+ */
+static
+int
+get_reduced_frame_map(WindowAggState *winstate, int64 pos)
+{
+ Assert(winstate->reduced_frame_map != NULL);
+ Assert(pos >= 0);
+
+ /*
+ * If pos is not in the reduced frame map, it means that any info
+ * regarding the pos has not been registered yet. So we return
+ * RF_NOT_DETERMINED.
+ */
+ if (pos >= winstate->alloc_sz)
+ return RF_NOT_DETERMINED;
+
+ return winstate->reduced_frame_map[pos];
+}
+
+/*
+ * register_reduced_frame_map
+ * Add/replace reduced frame map member at pos.
+ * If there's no enough space, expand the map.
+ */
+static
+void
+register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
+{
+ int64 realloc_sz;
+
+ Assert(winstate->reduced_frame_map != NULL);
+
+ if (pos < 0)
+ elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
+
+ if (pos > winstate->alloc_sz - 1)
+ {
+ realloc_sz = winstate->alloc_sz * 2;
+
+ winstate->reduced_frame_map =
+ repalloc(winstate->reduced_frame_map, realloc_sz);
+
+ MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
+ RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
+
+ winstate->alloc_sz = realloc_sz;
+ }
+
+ winstate->reduced_frame_map[pos] = val;
+}
+
+/*
+ * update_reduced_frame
+ * Update reduced frame info.
+ */
+static
+void
+update_reduced_frame(WindowObject winobj, int64 pos)
+{
+ WindowAggState *winstate = winobj->winstate;
+ ListCell *lc1,
+ *lc2;
+ bool expression_result;
+ int num_matched_rows;
+ int64 original_pos;
+ bool anymatch;
+ StringInfo encoded_str;
+ StringInfo pattern_str;
+ StringSet *str_set;
+ int initial_index;
+ VariablePos *variable_pos;
+ bool greedy = false;
+ int64 result_pos,
+ i;
+
+ /*
+ * Set of pattern variables evaluated to true. Each character corresponds
+ * to pattern variable. Example: str_set[0] = "AB"; str_set[1] = "AC"; In
+ * this case at row 0 A and B are true, and A and C are true in row 1.
+ */
+
+ /* initialize pattern variables set */
+ str_set = string_set_init();
+
+ /* save original pos */
+ original_pos = pos;
+
+ /*
+ * Check if the pattern does not include any greedy quantifier. If it does
+ * not, we can just apply the pattern to each row. If it succeeds, we are
+ * done.
+ */
+ foreach(lc1, winstate->patternRegexpList)
+ {
+ char *quantifier = strVal(lfirst(lc1));
+
+ if (*quantifier == '+' || *quantifier == '*')
+ {
+ greedy = true;
+ break;
+ }
+ }
+
+ /*
+ * Non greedy case
+ */
+ if (!greedy)
+ {
+ num_matched_rows = 0;
+
+ foreach(lc1, winstate->patternVariableList)
+ {
+ char *vname = strVal(lfirst(lc1));
+
+ encoded_str = makeStringInfo();
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "pos: " INT64_FORMAT " pattern vname: %s",
+ pos, vname);
+#endif
+ expression_result = false;
+
+ /* evaluate row pattern against current row */
+ result_pos = evaluate_pattern(winobj, pos, vname,
+ encoded_str, &expression_result);
+ destroyStringInfo(encoded_str);
+
+ if (!expression_result || result_pos < 0)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "expression result is false or out of frame");
+#endif
+ register_reduced_frame_map(winstate, original_pos,
+ RF_UNMATCHED);
+ return;
+ }
+ /* move to next row */
+ pos++;
+ num_matched_rows++;
+ }
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "pattern matched");
+#endif
+ register_reduced_frame_map(winstate, original_pos, RF_FRAME_HEAD);
+
+ for (i = original_pos + 1; i < original_pos + num_matched_rows; i++)
+ {
+ register_reduced_frame_map(winstate, i, RF_SKIPPED);
+ }
+ return;
+ }
+
+ /*
+ * Greedy quantifiers included. Loop over until none of pattern matches or
+ * encounters end of frame.
+ */
+ for (;;)
+ {
+ result_pos = -1;
+
+ /*
+ * Loop over each PATTERN variable.
+ */
+ anymatch = false;
+ encoded_str = makeStringInfo();
+
+ forboth(lc1, winstate->patternVariableList, lc2,
+ winstate->patternRegexpList)
+ {
+ char *vname = strVal(lfirst(lc1));
+#ifdef RPR_DEBUG
+ char *quantifier = strVal(lfirst(lc2));
+
+ elog(DEBUG1, "pos: " INT64_FORMAT " pattern vname: %s quantifier: %s",
+ pos, vname, quantifier);
+#endif
+ expression_result = false;
+
+ /* evaluate row pattern against current row */
+ result_pos = evaluate_pattern(winobj, pos, vname,
+ encoded_str, &expression_result);
+ if (expression_result)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "expression result is true");
+#endif
+ anymatch = true;
+ }
+
+ /*
+ * If out of frame, we are done.
+ */
+ if (result_pos < 0)
+ break;
+ }
+
+ if (!anymatch)
+ {
+ /* none of patterns matched. */
+ break;
+ }
+
+ string_set_add(str_set, encoded_str);
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "pos: " INT64_FORMAT " encoded_str: %s",
+ encoded_str->data);
+#endif
+
+ /* move to next row */
+ pos++;
+
+ if (result_pos < 0)
+ {
+ /* out of frame */
+ break;
+ }
+ }
+
+ if (string_set_get_size(str_set) == 0)
+ {
+ /* no match found in the first row */
+ register_reduced_frame_map(winstate, original_pos, RF_UNMATCHED);
+ destroyStringInfo(encoded_str);
+ return;
+ }
+
+#ifdef RPR_DEBUG
+ elog(DEBUG2, "pos: " INT64_FORMAT " encoded_str: %s",
+ pos, encoded_str->data);
+#endif
+
+ /* build regular expression */
+ pattern_str = makeStringInfo();
+ appendStringInfoChar(pattern_str, '^');
+ initial_index = 0;
+
+ variable_pos = variable_pos_init();
+
+ forboth(lc1, winstate->patternVariableList,
+ lc2, winstate->patternRegexpList)
+ {
+ char *vname = strVal(lfirst(lc1));
+ char *quantifier = strVal(lfirst(lc2));
+ char initial;
+
+ initial = pattern_initial(winstate, vname);
+ Assert(initial != 0);
+ appendStringInfoChar(pattern_str, initial);
+ if (quantifier[0])
+ appendStringInfoChar(pattern_str, quantifier[0]);
+
+ /*
+ * Register the initial at initial_index. If the initial appears more
+ * than once, all of it's initial_index will be recorded. This could
+ * happen if a pattern variable appears in the PATTERN clause more
+ * than once like "UP DOWN UP" "UP UP UP".
+ */
+ variable_pos_register(variable_pos, initial, initial_index);
+
+ initial_index++;
+ }
+
+#ifdef RPR_DEBUG
+ elog(DEBUG2, "pos: " INT64_FORMAT " pattern: %s",
+ pos, pattern_str->data);
+#endif
+
+ /* look for matching pattern variable sequence */
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "search_str_set started");
+#endif
+ num_matched_rows = search_str_set(pattern_str->data,
+ str_set, variable_pos);
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "search_str_set returns: %d", num_matched_rows);
+#endif
+ variable_pos_discard(variable_pos);
+ string_set_discard(str_set);
+
+ /*
+ * We are at the first row in the reduced frame. Save the number of
+ * matched rows as the number of rows in the reduced frame.
+ */
+ if (num_matched_rows <= 0)
+ {
+ /* no match */
+ register_reduced_frame_map(winstate, original_pos, RF_UNMATCHED);
+ }
+ else
+ {
+ register_reduced_frame_map(winstate, original_pos, RF_FRAME_HEAD);
+
+ for (i = original_pos + 1; i < original_pos + num_matched_rows; i++)
+ {
+ register_reduced_frame_map(winstate, i, RF_SKIPPED);
+ }
+ }
+
+ destroyStringInfo(pattern_str);
+
+ return;
+}
+
+/*
+ * search_str_set
+ * Perform pattern matching using "pattern" against str_set. pattern is a
+ * regular expression derived from PATTERN clause. Note that the regular
+ * expression string is prefixed by '^' and followed by initials represented
+ * in a same way as str_set. str_set is a set of StringInfo. Each StringInfo
+ * has a string comprising initials of pattern variable strings being true in
+ * a row. The initials are one of [a-y], parallel to the order of variable
+ * names in DEFINE clause. Suppose DEFINE has variables START, UP and DOWN. If
+ * PATTERN has START, UP+ and DOWN, then the initials in PATTERN will be 'a',
+ * 'b' and 'c'. The "pattern" will be "^ab+c".
+ *
+ * variable_pos is an array representing the order of pattern variable string
+ * initials in PATTERN clause. For example initial 'a' potion is in
+ * variable_pos[0].pos[0] = 0. Note that if the pattern is "START UP DOWN UP"
+ * (UP appears twice), then "UP" (initial is 'b') has two position 1 and
+ * 3. Thus variable_pos for b is variable_pos[1].pos[0] = 1 and
+ * variable_pos[1].pos[1] = 3.
+ *
+ * Returns the longest number of the matching rows (greedy matching) if
+ * quatifier '+' or '*' is included in "pattern".
+ */
+static
+int
+search_str_set(char *pattern, StringSet *str_set, VariablePos *variable_pos)
+{
+#define MAX_CANDIDATE_NUM 10000 /* max pattern match candidate size */
+#define FREEZED_CHAR 'Z' /* a pattern is freezed if it ends with the
+ * char */
+#define DISCARD_CHAR 'z' /* a pattern is not need to keep */
+
+ int set_size; /* number of rows in the set */
+ int resultlen;
+ int index;
+ StringSet *old_str_set,
+ *new_str_set;
+ int new_str_size;
+ int len;
+
+ set_size = string_set_get_size(str_set);
+ new_str_set = string_set_init();
+ len = 0;
+ resultlen = 0;
+
+ /*
+ * Generate all possible pattern variable name initials as a set of
+ * StringInfo named "new_str_set". For example, if we have two rows
+ * having "ab" (row 0) and "ac" (row 1) in the input str_set, new_str_set
+ * will have set of StringInfo "aa", "ac", "ba" and "bc" in the end.
+ */
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "pattern: %s set_size: %d", pattern, set_size);
+#endif
+ for (index = 0; index < set_size; index++)
+ {
+ StringInfo str; /* search target row */
+ char *p;
+ int old_set_size;
+ int i;
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "index: %d", index);
+#endif
+ if (index == 0)
+ {
+ /* copy variables in row 0 */
+ str = string_set_get(str_set, index);
+ p = str->data;
+
+ /*
+ * Loop over each new pattern variable char.
+ */
+ while (*p)
+ {
+ StringInfo new = makeStringInfo();
+
+ /* add pattern variable char */
+ appendStringInfoChar(new, *p);
+ /* add new one to string set */
+ string_set_add(new_str_set, new);
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "old_str: NULL new_str: %s", new->data);
+#endif
+ p++; /* next pattern variable */
+ }
+ }
+ else /* index != 0 */
+ {
+ old_str_set = new_str_set;
+ new_str_set = string_set_init();
+ str = string_set_get(str_set, index);
+ old_set_size = string_set_get_size(old_str_set);
+
+ /*
+ * Loop over each rows in the previous result set.
+ */
+ for (i = 0; i < old_set_size; i++)
+ {
+ StringInfo new;
+ char last_old_char;
+ int old_str_len;
+ StringInfo old = string_set_get(old_str_set, i);
+
+ p = old->data;
+ old_str_len = strlen(p);
+ if (old_str_len > 0)
+ last_old_char = p[old_str_len - 1];
+ else
+ last_old_char = '\0';
+
+ /* Is this old set freezed? */
+ if (last_old_char == FREEZED_CHAR)
+ {
+ /* if shorter match. we can discard it */
+ if ((old_str_len - 1) < resultlen)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "discard this old set because shorter match: %s",
+ old->data);
+#endif
+ continue;
+ }
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "keep this old set: %s", old->data);
+#endif
+
+ /* move the old set to new_str_set */
+ string_set_add(new_str_set, old);
+ old_str_set->str_set[i] = NULL;
+ continue;
+ }
+ /* Can this old set be discarded? */
+ else if (last_old_char == DISCARD_CHAR)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "discard this old set: %s", old->data);
+#endif
+ continue;
+ }
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "str->data: %s", str->data);
+#endif
+
+ /*
+ * loop over each pattern variable initial char in the input
+ * set.
+ */
+ for (p = str->data; *p; p++)
+ {
+ /*
+ * Optimization. Check if the row's pattern variable
+ * initial character position is greater than or equal to
+ * the old set's last pattern variable initial character
+ * position. For example, if the old set's last pattern
+ * variable initials are "ab", then the new pattern
+ * variable initial can be "b" or "c" but can not be "a",
+ * if the initials in PATTERN is something like "a b c" or
+ * "a b+ c+" etc. This optimization is possible when we
+ * only allow "+" quantifier.
+ */
+ if (variable_pos_compare(variable_pos, last_old_char, *p))
+ {
+ /* copy source string */
+ new = makeStringInfo();
+ enlargeStringInfo(new, old->len + 1);
+ appendStringInfoString(new, old->data);
+ /* add pattern variable char */
+ appendStringInfoChar(new, *p);
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "old_str: %s new_str: %s",
+ old->data, new->data);
+#endif
+
+ /*
+ * Adhoc optimization. If the first letter in the
+ * input string is the first and second position one
+ * and there's no associated quatifier '+', then we
+ * can dicard the input because there's no chance to
+ * expand the string further.
+ *
+ * For example, pattern "abc" cannot match "aa".
+ */
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "pattern[1]:%c pattern[2]:%c new[0]:%c new[1]:%c",
+ pattern[1], pattern[2], new->data[0], new->data[1]);
+#endif
+ if (pattern[1] == new->data[0] &&
+ pattern[1] == new->data[1] &&
+ pattern[2] != '+' &&
+ pattern[1] != pattern[2])
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "discard this new data: %s",
+ new->data);
+#endif
+ destroyStringInfo(new);
+ continue;
+ }
+
+ /* add new one to string set */
+ string_set_add(new_str_set, new);
+ }
+ else
+ {
+ /*
+ * We are freezing this pattern string. Since there's
+ * no chance to expand the string further, we perform
+ * pattern matching against the string. If it does not
+ * match, we can discard it.
+ */
+ len = do_pattern_match(pattern, old->data, old->len);
+
+ if (len <= 0)
+ {
+ /* no match. we can discard it */
+ continue;
+ }
+
+ else if (len <= resultlen)
+ {
+ /* shorter match. we can discard it */
+ continue;
+ }
+ else
+ {
+ /* match length is the longest so far */
+
+ int new_index;
+
+ /* remember the longest match */
+ resultlen = len;
+
+ /* freeze the pattern string */
+ new = makeStringInfo();
+ enlargeStringInfo(new, old->len + 1);
+ appendStringInfoString(new, old->data);
+ /* add freezed mark */
+ appendStringInfoChar(new, FREEZED_CHAR);
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "old_str: %s new_str: %s", old->data, new->data);
+#endif
+ string_set_add(new_str_set, new);
+
+ /*
+ * Search new_str_set to find out freezed entries
+ * that have shorter match length. Mark them as
+ * "discard" so that they are discarded in the
+ * next round.
+ */
+
+ /* new_index_size should be the one before */
+ new_str_size =
+ string_set_get_size(new_str_set) - 1;
+
+ /* loop over new_str_set */
+ for (new_index = 0; new_index < new_str_size;
+ new_index++)
+ {
+ char new_last_char;
+ int new_str_len;
+
+ new = string_set_get(new_str_set, new_index);
+ new_str_len = strlen(new->data);
+ if (new_str_len > 0)
+ {
+ new_last_char =
+ new->data[new_str_len - 1];
+ if (new_last_char == FREEZED_CHAR &&
+ (new_str_len - 1) <= len)
+ {
+ /*
+ * mark this set to discard in the
+ * next round
+ */
+ appendStringInfoChar(new, DISCARD_CHAR);
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "add discard char: %s", new->data);
+#endif
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ /* we no longer need old string set */
+ string_set_discard(old_str_set);
+ }
+ }
+
+ /*
+ * Perform pattern matching to find out the longest match.
+ */
+ new_str_size = string_set_get_size(new_str_set);
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "new_str_size: %d", new_str_size);
+#endif
+ len = 0;
+ resultlen = 0;
+
+ for (index = 0; index < new_str_size; index++)
+ {
+ StringInfo s;
+
+ s = string_set_get(new_str_set, index);
+ if (s == NULL)
+ continue; /* no data */
+
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "target string: %s", s->data);
+#endif
+
+ /*
+ * If the string is already freeze, we don't need to check it by
+ * do_pattern_match because it has been ready checked.
+ */
+ if (s->data[s->len] == FREEZED_CHAR)
+ len = s->len - 1;
+
+ /*
+ * If the string is scheduled to be discarded, we just disregard it.
+ */
+ else if (s->data[s->len] == DISCARD_CHAR)
+ continue;
+
+ else
+ len = do_pattern_match(pattern, s->data, s->len);
+
+ if (len > resultlen)
+ {
+ /* remember the longest match */
+ resultlen = len;
+
+ /*
+ * If the size of result set is equal to the number of rows in the
+ * set, we are done because it's not possible that the number of
+ * matching rows exceeds the number of rows in the set.
+ */
+ if (resultlen >= set_size)
+ break;
+ }
+ }
+
+ /* we no longer need new string set */
+ string_set_discard(new_str_set);
+
+ do_pattern_match_finish();
+ return resultlen;
+}
+
+/*
+ * do_pattern_match perform pattern match using pattern against encoded_str
+ * whose length is len bytes (without null terminate). returns matching
+ * number of rows if matching is succeeded. Otherwise returns 0.
+ */
+static
+int
+do_pattern_match(char *pattern, char *encoded_str, int len)
+{
+ static regex_t preg;
+ int plen;
+ int cflags = REG_EXTENDED;
+ size_t nmatch = 1;
+ int eflags = 0;
+ regmatch_t pmatch[1];
+ int sts;
+ pg_wchar *data;
+ int data_len;
+
+
+ /*
+ * Compile regexp if it does not exist.
+ */
+ if (regcache == NULL)
+ {
+ /* we need to convert to char to pg_wchar */
+ plen = strlen(pattern);
+ data = (pg_wchar *) palloc((plen + 1) * sizeof(pg_wchar));
+ data_len = pg_mb2wchar_with_len(pattern, data, plen);
+ /* compile re */
+ sts = pg_regcomp(&preg, /* compiled re */
+ data, /* target pattern */
+ data_len, /* length of pattern */
+ cflags, /* compile option */
+ C_COLLATION_OID /* collation */
+ );
+ pfree(data);
+
+ if (sts != REG_OKAY)
+ {
+ /* re didn't compile (no need for pg_regfree, if so) */
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("invalid regular expression: %s", pattern)));
+ }
+ regcache = &preg;
+ }
+
+ data = (pg_wchar *) palloc((strlen(encoded_str) + 1) * sizeof(pg_wchar));
+ data_len = pg_mb2wchar_with_len(encoded_str, data, len);
+
+ /* execute the regular expression match */
+ sts = pg_regexec(
+ &preg, /* compiled re */
+ data, /* target string */
+ data_len, /* length of encoded_str */
+ 0, /* search start */
+ NULL, /* rm details */
+ nmatch, /* number of match sub re */
+ pmatch, /* match result details */
+ eflags);
+
+ pfree(data);
+
+ if (sts != REG_OKAY)
+ {
+ if (sts != REG_NOMATCH)
+ {
+ char errMsg[100];
+
+ pg_regerror(sts, &preg, errMsg, sizeof(errMsg));
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("regular expression failed: %s", errMsg)));
+ }
+ return 0; /* does not match */
+ }
+
+ len = pmatch[0].rm_eo; /* return match length */
+ return len;
+
+}
+
+static
+void
+do_pattern_match_finish(void)
+{
+ if (regcache != NULL)
+ pg_regfree(regcache);
+ regcache = NULL;
+}
+
+/*
+ * evaluate_pattern
+ * Evaluate expression associated with PATTERN variable vname. current_pos is
+ * relative row position in a frame (starting from 0). If vname is evaluated
+ * to true, initial letters associated with vname is appended to
+ * encode_str. result is out paramater representing the expression evaluation
+ * result is true of false.
+ *---------
+ * Return values are:
+ * >=0: the last match absolute row position
+ * otherwise out of frame.
+ *---------
+ */
+static
+int64
+evaluate_pattern(WindowObject winobj, int64 current_pos,
+ char *vname, StringInfo encoded_str, bool *result)
+{
+ WindowAggState *winstate = winobj->winstate;
+ ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ ListCell *lc1,
+ *lc2,
+ *lc3;
+ ExprState *pat;
+ Datum eval_result;
+ bool out_of_frame = false;
+ bool isnull;
+ TupleTableSlot *slot;
+
+ forthree(lc1, winstate->defineVariableList,
+ lc2, winstate->defineClauseList,
+ lc3, winstate->defineInitial)
+ {
+ char initial; /* initial letter associated with vname */
+ char *name = strVal(lfirst(lc1));
+
+ if (strcmp(vname, name))
+ continue;
+
+ initial = *(strVal(lfirst(lc3)));
+
+ /* set expression to evaluate */
+ pat = lfirst(lc2);
+
+ /* get current, previous and next tuples */
+ if (!get_slots(winobj, current_pos))
+ {
+ out_of_frame = true;
+ }
+ else
+ {
+ /* evaluate the expression */
+ eval_result = ExecEvalExpr(pat, econtext, &isnull);
+ if (isnull)
+ {
+ /* expression is NULL */
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "expression for %s is NULL at row: " INT64_FORMAT,
+ vname, current_pos);
+#endif
+ *result = false;
+ }
+ else
+ {
+ if (!DatumGetBool(eval_result))
+ {
+ /* expression is false */
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "expression for %s is false at row: " INT64_FORMAT,
+ vname, current_pos);
+#endif
+ *result = false;
+ }
+ else
+ {
+ /* expression is true */
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "expression for %s is true at row: " INT64_FORMAT,
+ vname, current_pos);
+#endif
+ appendStringInfoChar(encoded_str, initial);
+ *result = true;
+ }
+ }
+
+ slot = winstate->temp_slot_1;
+ if (slot != winstate->null_slot)
+ ExecClearTuple(slot);
+ slot = winstate->prev_slot;
+ if (slot != winstate->null_slot)
+ ExecClearTuple(slot);
+ slot = winstate->next_slot;
+ if (slot != winstate->null_slot)
+ ExecClearTuple(slot);
+
+ break;
+ }
+
+ if (out_of_frame)
+ {
+ *result = false;
+ return -1;
+ }
+ }
+ return current_pos;
+}
+
+/*
+ * get_slots
+ * Get current, previous and next tuples.
+ * Returns false if current row is out of partition/full frame.
+ */
+static
+bool
+get_slots(WindowObject winobj, int64 current_pos)
+{
+ WindowAggState *winstate = winobj->winstate;
+ TupleTableSlot *slot;
+ int ret;
+ ExprContext *econtext;
+
+ econtext = winstate->ss.ps.ps_ExprContext;
+
+ /* set up current row tuple slot */
+ slot = winstate->temp_slot_1;
+ if (!window_gettupleslot(winobj, current_pos, slot))
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "current row is out of partition at:" INT64_FORMAT,
+ current_pos);
+#endif
+ return false;
+ }
+ ret = row_is_in_frame(winstate, current_pos, slot);
+ if (ret <= 0)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "current row is out of frame at: " INT64_FORMAT,
+ current_pos);
+#endif
+ ExecClearTuple(slot);
+ return false;
+ }
+ econtext->ecxt_outertuple = slot;
+
+ /* for PREV */
+ if (current_pos > 0)
+ {
+ slot = winstate->prev_slot;
+ if (!window_gettupleslot(winobj, current_pos - 1, slot))
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "previous row is out of partition at: " INT64_FORMAT,
+ current_pos - 1);
+#endif
+ econtext->ecxt_scantuple = winstate->null_slot;
+ }
+ else
+ {
+ ret = row_is_in_frame(winstate, current_pos - 1, slot);
+ if (ret <= 0)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "previous row is out of frame at: " INT64_FORMAT,
+ current_pos - 1);
+#endif
+ ExecClearTuple(slot);
+ econtext->ecxt_scantuple = winstate->null_slot;
+ }
+ else
+ {
+ econtext->ecxt_scantuple = slot;
+ }
+ }
+ }
+ else
+ econtext->ecxt_scantuple = winstate->null_slot;
+
+ /* for NEXT */
+ slot = winstate->next_slot;
+ if (!window_gettupleslot(winobj, current_pos + 1, slot))
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "next row is out of partiton at: " INT64_FORMAT,
+ current_pos + 1);
+#endif
+ econtext->ecxt_innertuple = winstate->null_slot;
+ }
+ else
+ {
+ ret = row_is_in_frame(winstate, current_pos + 1, slot);
+ if (ret <= 0)
+ {
+#ifdef RPR_DEBUG
+ elog(DEBUG1, "next row is out of frame at: " INT64_FORMAT,
+ current_pos + 1);
+#endif
+ ExecClearTuple(slot);
+ econtext->ecxt_innertuple = winstate->null_slot;
+ }
+ else
+ econtext->ecxt_innertuple = slot;
+ }
+ return true;
+}
+
+/*
+ * pattern_initial
+ * Return pattern variable initial character
+ * matching with pattern variable name vname.
+ * If not found, return 0.
+ */
+static
+char
+pattern_initial(WindowAggState *winstate, char *vname)
+{
+ char initial;
+ char *name;
+ ListCell *lc1,
+ *lc2;
+
+ forboth(lc1, winstate->defineVariableList,
+ lc2, winstate->defineInitial)
+ {
+ name = strVal(lfirst(lc1)); /* DEFINE variable name */
+ initial = *(strVal(lfirst(lc2))); /* DEFINE variable initial */
+
+
+ if (!strcmp(name, vname))
+ return initial; /* found */
+ }
+ return 0;
+}
+
+/*
+ * string_set_init
+ * Create dynamic set of StringInfo.
+ */
+static
+StringSet *
+string_set_init(void)
+{
+/* Initial allocation size of str_set */
+#define STRING_SET_ALLOC_SIZE 1024
+
+ StringSet *string_set;
+ Size set_size;
+
+ string_set = palloc0(sizeof(StringSet));
+ string_set->set_index = 0;
+ set_size = STRING_SET_ALLOC_SIZE;
+ string_set->str_set = palloc(set_size * sizeof(StringInfo));
+ string_set->set_size = set_size;
+
+ return string_set;
+}
+
+/*
+ * string_set_add
+ * Add StringInfo str to StringSet string_set.
+ */
+static
+void
+string_set_add(StringSet *string_set, StringInfo str)
+{
+ Size set_size;
+
+ set_size = string_set->set_size;
+ if (string_set->set_index >= set_size)
+ {
+ set_size *= 2;
+ string_set->str_set = repalloc(string_set->str_set,
+ set_size * sizeof(StringInfo));
+ string_set->set_size = set_size;
+ }
+
+ string_set->str_set[string_set->set_index++] = str;
+
+ return;
+}
+
+/*
+ * string_set_get
+ * Returns StringInfo specified by index.
+ * If there's no data yet, returns NULL.
+ */
+static
+StringInfo
+string_set_get(StringSet *string_set, int index)
+{
+ /* no data? */
+ if (index == 0 && string_set->set_index == 0)
+ return NULL;
+
+ if (index < 0 || index >= string_set->set_index)
+ elog(ERROR, "invalid index: %d", index);
+
+ return string_set->str_set[index];
+}
+
+/*
+ * string_set_get_size
+ * Returns the size of StringSet.
+ */
+static
+int
+string_set_get_size(StringSet *string_set)
+{
+ return string_set->set_index;
+}
+
+/*
+ * string_set_discard
+ * Discard StringSet.
+ * All memory including StringSet itself is freed.
+ */
+static
+void
+string_set_discard(StringSet *string_set)
+{
+ int i;
+
+ for (i = 0; i < string_set->set_index; i++)
+ {
+ StringInfo str = string_set->str_set[i];
+
+ if (str)
+ destroyStringInfo(str);
+ }
+ pfree(string_set->str_set);
+ pfree(string_set);
+}
+
+/*
+ * variable_pos_init
+ * Create and initialize variable postion structure
+ */
+static
+VariablePos *
+variable_pos_init(void)
+{
+ VariablePos *variable_pos;
+
+ variable_pos = palloc(sizeof(VariablePos) * NUM_ALPHABETS);
+ MemSet(variable_pos, -1, sizeof(VariablePos) * NUM_ALPHABETS);
+ return variable_pos;
+}
+
+/*
+ * variable_pos_register
+ * Register pattern variable whose initial is initial into postion index.
+ * pos is position of initial.
+ * If pos is already registered, register it at next empty slot.
+ */
+static
+void
+variable_pos_register(VariablePos *variable_pos, char initial, int pos)
+{
+ int index = initial - 'a';
+ int slot;
+ int i;
+
+ if (pos < 0 || pos > NUM_ALPHABETS)
+ elog(ERROR, "initial is not valid char: %c", initial);
+
+ for (i = 0; i < NUM_ALPHABETS; i++)
+ {
+ slot = variable_pos[index].pos[i];
+ if (slot < 0)
+ {
+ /* empty slot found */
+ variable_pos[index].pos[i] = pos;
+ return;
+ }
+ }
+ elog(ERROR, "no empty slot for initial: %c", initial);
+}
+
+/*
+ * variable_pos_compare
+ * Returns true if initial1 can be followed by initial2
+ */
+static
+bool
+variable_pos_compare(VariablePos *variable_pos, char initial1, char initial2)
+{
+ int index1,
+ index2;
+ int pos1,
+ pos2;
+
+ for (index1 = 0;; index1++)
+ {
+ pos1 = variable_pos_fetch(variable_pos, initial1, index1);
+ if (pos1 < 0)
+ break;
+
+ for (index2 = 0;; index2++)
+ {
+ pos2 = variable_pos_fetch(variable_pos, initial2, index2);
+ if (pos2 < 0)
+ break;
+ if (pos1 <= pos2)
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * variable_pos_fetch
+ * Fetch position of pattern variable whose initial is initial, and whose index
+ * is index. If no postion was registered by initial, index, returns -1.
+ */
+static
+int
+variable_pos_fetch(VariablePos *variable_pos, char initial, int index)
+{
+ int pos = initial - 'a';
+
+ if (pos < 0 || pos > NUM_ALPHABETS)
+ elog(ERROR, "initial is not valid char: %c", initial);
+
+ if (index < 0 || index > NUM_ALPHABETS)
+ elog(ERROR, "index is not valid: %d", index);
+
+ return variable_pos[pos].pos[index];
+}
+
+/*
+ * variable_pos_discard
+ * Discard VariablePos
+ */
+static
+void
+variable_pos_discard(VariablePos *variable_pos)
+{
+ pfree(variable_pos);
+}
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 473c61569f..3142a8bc06 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -13,6 +13,9 @@
*/
#include "postgres.h"
+#include "catalog/pg_collation_d.h"
+#include "executor/executor.h"
+#include "nodes/execnodes.h"
#include "nodes/parsenodes.h"
#include "nodes/supportnodes.h"
#include "utils/fmgrprotos.h"
@@ -37,11 +40,19 @@ typedef struct
int64 remainder; /* (total rows) % (bucket num) */
} ntile_context;
+/*
+ * rpr process information.
+ * Used for AFTER MATCH SKIP PAST LAST ROW
+ */
+typedef struct SkipContext
+{
+ int64 pos; /* last row absolute position */
+} SkipContext;
+
static bool rank_up(WindowObject winobj);
static Datum leadlag_common(FunctionCallInfo fcinfo,
bool forward, bool withoffset, bool withdefault);
-
/*
* utility routine for *_rank functions.
*/
@@ -674,7 +685,7 @@ window_last_value(PG_FUNCTION_ARGS)
bool isnull;
result = WinGetFuncArgInFrame(winobj, 0,
- 0, WINDOW_SEEK_TAIL, true,
+ 0, WINDOW_SEEK_TAIL, false,
&isnull, NULL);
if (isnull)
PG_RETURN_NULL();
@@ -714,3 +725,25 @@ window_nth_value(PG_FUNCTION_ARGS)
PG_RETURN_DATUM(result);
}
+
+/*
+ * prev
+ * Dummy function to invoke RPR's navigation operator "PREV".
+ * This is *not* a window function.
+ */
+Datum
+window_prev(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+}
+
+/*
+ * next
+ * Dummy function to invoke RPR's navigation operation "NEXT".
+ * This is *not* a window function.
+ */
+Datum
+window_next(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2dcc2d42da..ee11ee2c8b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10664,6 +10664,12 @@
{ oid => '3114', descr => 'fetch the Nth row value',
proname => 'nth_value', prokind => 'w', prorettype => 'anyelement',
proargtypes => 'anyelement int4', prosrc => 'window_nth_value' },
+{ oid => '8126', descr => 'previous value',
+ proname => 'prev', provolatile => 's', prorettype => 'anyelement',
+ proargtypes => 'anyelement', prosrc => 'window_prev' },
+{ oid => '8127', descr => 'next value',
+ proname => 'next', provolatile => 's', prorettype => 'anyelement',
+ proargtypes => 'anyelement', prosrc => 'window_next' },
# functions for range types
{ oid => '3832', descr => 'I/O',
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 79d5e96021..87591a8d43 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2585,6 +2585,11 @@ typedef enum WindowAggStatus
* tuples during spool */
} WindowAggStatus;
+#define RF_NOT_DETERMINED 0
+#define RF_FRAME_HEAD 1
+#define RF_SKIPPED 2
+#define RF_UNMATCHED 3
+
typedef struct WindowAggState
{
ScanState ss; /* its first field is NodeTag */
@@ -2644,6 +2649,19 @@ typedef struct WindowAggState
int64 groupheadpos; /* current row's peer group head position */
int64 grouptailpos; /* " " " " tail position (group end+1) */
+ /* these fields are used in Row pattern recognition: */
+ RPSkipTo rpSkipTo; /* Row Pattern Skip To type */
+ List *patternVariableList; /* list of row pattern variables names
+ * (list of String) */
+ List *patternRegexpList; /* list of row pattern regular expressions
+ * ('+' or ''. list of String) */
+ List *defineVariableList; /* list of row pattern definition
+ * variables (list of String) */
+ List *defineClauseList; /* expression for row pattern definition
+ * search conditions ExprState list */
+ List *defineInitial; /* list of row pattern definition variable
+ * initials (list of String) */
+
MemoryContext partcontext; /* context for partition-lifespan data */
MemoryContext aggcontext; /* shared context for aggregate working data */
MemoryContext curaggcontext; /* current aggregate's working data */
@@ -2671,6 +2689,18 @@ typedef struct WindowAggState
TupleTableSlot *agg_row_slot;
TupleTableSlot *temp_slot_1;
TupleTableSlot *temp_slot_2;
+
+ /* temporary slots for RPR */
+ TupleTableSlot *prev_slot; /* PREV row navigation operator */
+ TupleTableSlot *next_slot; /* NEXT row navigation operator */
+ TupleTableSlot *null_slot; /* all NULL slot */
+
+ /*
+ * Each byte corresponds to a row positioned at absolute its pos in
+ * partition. See above definition for RF_*
+ */
+ char *reduced_frame_map;
+ int64 alloc_sz; /* size of the map */
} WindowAggState;
/* ----------------
--
2.25.1
----Next_Part(Sat_Dec_21_18_20_04_2024_526)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v25-0006-Row-pattern-recognition-patch-docs.patch"
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* [PATCH v1] Add per-backend AIO statistics
@ 2026-06-11 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-06-11 09:45 UTC (permalink / raw)
This commit adds per-backend AIO statistics, providing per-backend AIO behavior
details.
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns the following counters based on the PID
provided in input:
- started: total number of AIO operations initiated
- executed_sync: IOs that were executed synchronously (fallback path)
- executed_async: IOs that were submitted asynchronously to the IO method
- completed_self: IO completions processed by the issuing backend itself
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times the backend had to wait for a free AIO handle
- submitted: number of submitted calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion
patterns. That helps see how IO completion work is distributed and could
help interpret per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
This commit is straight-forward, relying on the infrastructure provided
by 9aea73fc61d4 (backend-level pgstats).
XXX: Bump catalog version. No need to touch PGSTAT_FILE_FORMAT_ID as backend
statistics are never written to disk.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 70 +++++++++++++
src/backend/storage/aio/aio.c | 13 +++
src/backend/utils/activity/pgstat_backend.c | 109 ++++++++++++++++++++
src/backend/utils/adt/pgstatfuncs.c | 60 +++++++++++
src/include/catalog/pg_proc.dat | 7 ++
src/include/pgstat.h | 25 +++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/modules/test_aio/t/001_aio.pl | 32 ++++++
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 319 insertions(+), 1 deletion(-)
24.4% doc/src/sgml/
3.2% src/backend/storage/aio/
25.3% src/backend/utils/activity/
20.5% src/backend/utils/adt/
4.9% src/include/catalog/
10.0% src/include/
11.1% src/test/modules/test_aio/t/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 12b9ee20d4a..1c566cf0e0e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5641,6 +5641,76 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-aio" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_aio</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_aio</function> ( <type>integer</type> )
+ <returnvalue>record</returnvalue>
+ </para>
+ <para>
+ Returns <acronym>AIO</acronym> (Asynchronous I/O) statistics about the
+ backend with the specified process ID. The returned values are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <literal>started</literal>: Total IOs initiated.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_sync</literal>: IOs executed synchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>executed_async</literal>: IOs submitted asynchronously.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_self</literal>: IO completions processed by this
+ backend.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>completed_other</literal>: IO completions processed on
+ behalf of other backends.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>handle_waits</literal>: Times waited for a free AIO handle.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>submitted</literal>: Number of submit calls to the IO
+ method. Compare with <literal>executed_async</literal> to determine
+ average batch size.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <literal>stats_reset</literal>: Timestamp of last stats reset.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ The <literal>completed_other</literal> column is only meaningful
+ when <varname>io_method</varname> is set to <literal>io_uring</literal>;
+ with <literal>worker</literal> mode, IO completions are processed by
+ IO worker processes which do not track these statistics.
+ </para>
+ <para>
+ The function does not return AIO statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c
index 8f7e26607b9..507f1727d41 100644
--- a/src/backend/storage/aio/aio.c
+++ b/src/backend/storage/aio/aio.c
@@ -40,6 +40,7 @@
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "pgstat.h"
#include "port/atomics.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
@@ -458,6 +459,8 @@ pgaio_io_stage(PgAioHandle *ioh, PgAioOp op)
"staged (synchronous: %d, in_batch: %d)",
needs_synchronous, pgaio_my_backend->in_batchmode);
+ pgstat_count_backend_aio_start(needs_synchronous);
+
if (!needs_synchronous)
{
pgaio_my_backend->staged_ios[pgaio_my_backend->num_staged_ios++] = ioh;
@@ -544,6 +547,12 @@ pgaio_io_process_completion(PgAioHandle *ioh, int result)
/* condition variable broadcast ensures state is visible before wakeup */
ConditionVariableBroadcast(&ioh->cv);
+ /* Track AIO completion stats */
+ if (ioh->owner_procno == MyProcNumber)
+ pgstat_count_backend_aio_complete_self();
+ else
+ pgstat_count_backend_aio_complete_other();
+
/* contains call to pgaio_io_call_complete_local() */
if (ioh->owner_procno == MyProcNumber)
pgaio_io_reclaim(ioh);
@@ -762,6 +771,8 @@ pgaio_io_wait_for_free(void)
{
int reclaimed = 0;
+ pgstat_count_backend_aio_handle_wait();
+
pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs",
pgaio_my_backend->num_staged_ios,
dclist_count(&pgaio_my_backend->in_flight_ios),
@@ -1150,6 +1161,8 @@ pgaio_submit_staged(void)
Assert(total_submitted == did_submit);
+ pgstat_count_backend_aio_submitted();
+
pgaio_my_backend->num_staged_ios = 0;
pgaio_debug(DEBUG4,
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index b736b2ccc6f..4ad755f7f18 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -40,6 +40,7 @@
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
static bool backend_has_lockstats = false;
+static bool backend_has_aiostats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -120,6 +121,74 @@ pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report AIO stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_aio_start(bool synchronous)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.started++;
+ if (synchronous)
+ PendingBackendStats.aio_counters.executed_sync++;
+ else
+ PendingBackendStats.aio_counters.executed_async++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_self(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_self++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_complete_other(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.completed_other++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_handle_wait(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.handle_waits++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_aio_submitted(void)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ PendingBackendStats.aio_counters.submitted++;
+
+ backend_has_aiostats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -326,6 +395,38 @@ pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
backend_has_lockstats = false;
}
+/*
+ * Flush out locally pending backend AIO statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_aio(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_AioCounters *bktype_shstats;
+
+ if (!backend_has_aiostats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.aio_counters;
+
+#define AIOSTAT_ACC(fld) \
+ (bktype_shstats->fld += PendingBackendStats.aio_counters.fld)
+ AIOSTAT_ACC(started);
+ AIOSTAT_ACC(executed_sync);
+ AIOSTAT_ACC(executed_async);
+ AIOSTAT_ACC(completed_self);
+ AIOSTAT_ACC(completed_other);
+ AIOSTAT_ACC(handle_waits);
+ AIOSTAT_ACC(submitted);
+#undef AIOSTAT_ACC
+
+ MemSet(&PendingBackendStats.aio_counters, 0, sizeof(PgStat_AioCounters));
+
+ backend_has_aiostats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -354,6 +455,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
has_pending_data = true;
+ /* Some AIO data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_AIO) && backend_has_aiostats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -372,6 +477,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
pgstat_flush_backend_entry_lock(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_AIO)
+ pgstat_flush_backend_entry_aio(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -411,6 +519,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
backend_has_lockstats = false;
+ backend_has_aiostats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..fb62a56f3fe 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1722,6 +1722,66 @@ pg_stat_get_backend_wal(PG_FUNCTION_ARGS)
return (pg_stat_wal_build_tuple(bktype_stats, backend_stats->stat_reset_timestamp));
}
+/*
+ * Returns AIO statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_aio(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_BACKEND_AIO_COLS 8
+ TupleDesc tupdesc;
+ Datum values[PG_STAT_BACKEND_AIO_COLS] = {0};
+ bool nulls[PG_STAT_BACKEND_AIO_COLS] = {0};
+ int pid;
+ PgStat_Backend *backend_stats;
+ PgStat_AioCounters aio_counters;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ PG_RETURN_NULL();
+
+ aio_counters = backend_stats->aio_counters;
+
+ /* Initialise attributes information in the tuple descriptor */
+ tupdesc = CreateTemplateTupleDesc(PG_STAT_BACKEND_AIO_COLS);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "started",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "executed_sync",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "executed_async",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "completed_self",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 5, "completed_other",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 6, "handle_waits",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 7, "submitted",
+ INT8OID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stats_reset",
+ TIMESTAMPTZOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+ BlessTupleDesc(tupdesc);
+
+ /* Fill values */
+ values[0] = Int64GetDatum(aio_counters.started);
+ values[1] = Int64GetDatum(aio_counters.executed_sync);
+ values[2] = Int64GetDatum(aio_counters.executed_async);
+ values[3] = Int64GetDatum(aio_counters.completed_self);
+ values[4] = Int64GetDatum(aio_counters.completed_other);
+ values[5] = Int64GetDatum(aio_counters.handle_waits);
+ values[6] = Int64GetDatum(aio_counters.submitted);
+
+ if (backend_stats->stat_reset_timestamp != 0)
+ values[7] = TimestampTzGetDatum(backend_stats->stat_reset_timestamp);
+ else
+ nulls[7] = true;
+
+ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..a089860ada4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6108,6 +6108,13 @@
proargmodes => '{i,o,o,o,o,o}',
proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
prosrc => 'pg_stat_get_backend_lock' },
+{ oid => '9082', descr => 'statistics: backend AIO activity',
+ proname => 'pg_stat_get_backend_aio', provolatile => 'v', proparallel => 'r',
+ prorettype => 'record', proargtypes => 'int4',
+ proallargtypes => '{int4,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_pid,started,executed_sync,executed_async,completed_self,completed_other,handle_waits,submitted,stats_reset}',
+ prosrc => 'pg_stat_get_backend_aio' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..64afb2bc082 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -514,6 +514,21 @@ typedef struct PgStat_WalStats
TimestampTz stat_reset_timestamp;
} PgStat_WalStats;
+/* -------
+ * PgStat_AioCounters AIO activity counters
+ * -------
+ */
+typedef struct PgStat_AioCounters
+{
+ PgStat_Counter started;
+ PgStat_Counter executed_sync;
+ PgStat_Counter executed_async;
+ PgStat_Counter completed_self;
+ PgStat_Counter completed_other;
+ PgStat_Counter handle_waits;
+ PgStat_Counter submitted;
+} PgStat_AioCounters;
+
/* -------
* PgStat_Backend Backend statistics
* -------
@@ -524,6 +539,7 @@ typedef struct PgStat_Backend
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
PgStat_PendingLock lock_stats;
+ PgStat_AioCounters aio_counters;
} PgStat_Backend;
/* ---------
@@ -542,6 +558,8 @@ typedef struct PgStat_BackendPending
* PGSTAT_KIND_LOCK.
*/
PgStat_PendingLock pending_lock;
+ /* Store the AIO statistics counters */
+ PgStat_AioCounters aio_counters;
} PgStat_BackendPending;
/*
@@ -598,6 +616,13 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+/* used by aio.c for AIO stats tracked in backends */
+extern void pgstat_count_backend_aio_start(bool synchronous);
+extern void pgstat_count_backend_aio_complete_self(void);
+extern void pgstat_count_backend_aio_complete_other(void);
+extern void pgstat_count_backend_aio_handle_wait(void);
+extern void pgstat_count_backend_aio_submitted(void);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index b3dc3ff7d8b..b2092fb42b9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -706,7 +706,8 @@ extern void pgstat_archiver_snapshot_cb(void);
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
+#define PGSTAT_BACKEND_FLUSH_AIO (1 << 3) /* Flush AIO statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK | PGSTAT_BACKEND_FLUSH_AIO)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..8ecf3d3ad91 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -1842,6 +1842,37 @@ read_buffers('$table', 0, 4)|,
$psql_c->quit();
}
+# Test per-backend AIO statistics counters
+sub test_aio_stats
+{
+ my $io_method = shift;
+ my $node = shift;
+
+ my $psql = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Reset backend stats, evict relation, then read it back to force
+ # physical IO through the AIO layer.
+ $psql->query_safe(qq(SELECT pg_stat_reset_backend_stats(pg_backend_pid())));
+ $psql->query_safe(qq(SELECT evict_rel('tbl_ok')));
+ $psql->query_safe(qq(SELECT count(*) FROM tbl_ok));
+ $psql->query_safe(qq(SELECT pg_stat_force_next_flush()));
+
+ # started must be > 0 after physical IO
+ my $started = $psql->query_safe(
+ qq(SELECT started FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ cmp_ok($started, '>', 0,
+ "$io_method: AIO stats: started > 0 after physical IO");
+
+ # invariant: started = executed_sync + executed_async
+ my $consistent = $psql->query_safe(
+ qq(SELECT started = executed_sync + executed_async
+ FROM pg_stat_get_backend_aio(pg_backend_pid())));
+ is($consistent, 't',
+ "$io_method: AIO stats: started = executed_sync + executed_async");
+
+ $psql->quit();
+}
+
# Run all tests that for the specified node / io_method
sub test_io_method
{
@@ -1878,6 +1909,7 @@ CHECKPOINT;
test_ignore_checksum($io_method, $node);
test_checksum_createdb($io_method, $node);
test_read_buffers($io_method, $node);
+ test_aio_stats($io_method, $node);
# generic injection tests
SKIP:
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..6ce3077fa5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2329,6 +2329,7 @@ PgStatShared_ReplSlot
PgStatShared_SLRU
PgStatShared_Subscription
PgStatShared_Wal
+PgStat_AioCounters
PgStat_ArchiverStats
PgStat_Backend
PgStat_BackendPending
--
2.34.1
--Pi0/Nu3LVhi0Ij+H--
^ permalink raw reply [nested|flat] 225+ messages in thread
* Add per-backend AIO statistics
@ 2026-07-07 11:02 Bertrand Drouvot <[email protected]>
0 siblings, 2 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-07-07 11:02 UTC (permalink / raw)
To: [email protected]
Hi hackers,
Currently to monitor AIO we can use:
1/ pg_aios that lists all AIO handles that are currently in use. That shows
what's happening right now, but not what has happened.
2/ pg_stat_get_backend_io() that shows how much IO was done, but not how it
was done. There's no way to see whether IOs ran synchronously or
asynchronously, whether a backend was stalling on handle exhaustion, or how
completions are distributed across backends.
This patch helps answering those questions by exposing cumulative per-backend
AIO counters:
- started: total AIO operations initiated
- executed_sync: IOs executed synchronously (fallback path)
- executed_async: IOs submitted asynchronously
- completed_self: IO completions processed by the issuing backend
- completed_other: IO completions processed on behalf of another backend
- handle_waits: times waited for a free AIO handle
- submitted: number of submit calls to the IO method
These counters are useful for understanding and tuning AIO behavior:
- executed_async / started. A ratio near zero means the backend is falling back
to synchronous execution (TOAST chunk fetches, temp buffers, ...).
- a non-zero handle_waits means the backend exhausted all its AIO handles. That
could mean that io_max_concurrency is too low.
- completed_self vs completed_other reveals cross-backend completion patterns.
That helps see how IO completion work is distributed and could help interpret
per backend IO statistics values.
- executed_async / submitted gives the average batch size per submit call.
As far as the technical implementation:
This data can be retrieved with a new system function called
pg_stat_get_backend_aio(), that returns one row based on the PID provided in input.
pgstat_flush_backend() gains a new flag value, able to control the flush of the
AIO stats.
This patch relies mostly on the infrastructure provided by 9aea73fc61d4, that
has introduced backend statistics.
The overhead (4 functions calls and counters increments) kind of follow the same
patterns as pgstat_count_backend_io_op() and I did not observe measurable
regression (I did not expect to). Also that does not add that much memory
per-backend: PgStat_AioCounters is 56 bytes.
There is no "double" counting as a global view to show those counters does not
exist. I think that's better to start with the per-backend side of it and see
if we want to also add a global view. For example, completed_other identifies
which backends did IOs for other backends. Also this allows correlating with
pg_stat_activity and pg_stat_get_backend_io().
Examples based on Franck's blog post [1]:
1/ query the smalldocs table:
postgres=# select count(*),avg(length(data)) from smalldocs;
count | avg
---------+-----------------------
1024000 | 1024.0000000000000000
(1 row)
postgres=# SELECT * FROM pg_stat_get_backend_aio(pg_backend_pid());
started | executed_sync | executed_async | completed_self | completed_other | handle_waits | submitted | stats_reset
---------+---------------+----------------+----------------+-----------------+--------------+-----------+-------------------------------
3125 | 46 | 3079 | 46 | 0 | 0 | 3078 | 2026-07-07 09:28:27.412136+00
We can see that the sequential scan fully benefits from AIO.
2/ query the largedocs table:
postgres=# select count(*),avg(length(data)) from largedocs;
count | avg
-------+----------------------
1000 | 1048576.000000000000
(1 row)
postgres=# SELECT * FROM pg_stat_get_backend_aio(pg_backend_pid());
started | executed_sync | executed_async | completed_self | completed_other | handle_waits | submitted | stats_reset
---------+---------------+----------------+----------------+-----------------+--------------+-----------+-------------------------------
121154 | 121150 | 4 | 121150 | 0 | 0 | 4 | 2026-07-07 09:35:00.504872+00
We can see that the sequential scan bypasses AIO.
Looking forward to your feedback.
[1]: https://dev.to/franckpachot/iouring-buffered-reads-in-postgresql-19-iouring-mcn
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 225+ messages in thread
* Re: Add per-backend AIO statistics
@ 2026-07-08 06:00 Bertrand Drouvot <[email protected]>
parent: Bertrand Drouvot <[email protected]>
1 sibling, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-07-08 06:00 UTC (permalink / raw)
To: [email protected]
Hi,
On Tue, Jul 07, 2026 at 11:02:03AM +0000, Bertrand Drouvot wrote:
> postgres=# select count(*),avg(length(data)) from smalldocs;
> count | avg
> ---------+-----------------------
> 1024000 | 1024.0000000000000000
> (1 row)
>
> postgres=# SELECT * FROM pg_stat_get_backend_aio(pg_backend_pid());
> started | executed_sync | executed_async | completed_self | completed_other | handle_waits | submitted | stats_reset
> ---------+---------------+----------------+----------------+-----------------+--------------+-----------+-------------------------------
> 3125 | 46 | 3079 | 46 | 0 | 0 | 3078 | 2026-07-07 09:28:27.412136+00
>
> We can see that the sequential scan fully benefits from AIO.
>
> 2/ query the largedocs table:
>
> postgres=# select count(*),avg(length(data)) from largedocs;
> count | avg
> -------+----------------------
> 1000 | 1048576.000000000000
> (1 row)
>
> postgres=# SELECT * FROM pg_stat_get_backend_aio(pg_backend_pid());
> started | executed_sync | executed_async | completed_self | completed_other | handle_waits | submitted | stats_reset
> ---------+---------------+----------------+----------------+-----------------+--------------+-----------+-------------------------------
> 121154 | 121150 | 4 | 121150 | 0 | 0 | 4 | 2026-07-07 09:35:00.504872+00
>
> We can see that the sequential scan bypasses AIO.
I was just doing some AIO experiments and was using the new pg_stat_get_backend_aio()
function.
So, while at it, sharing more examples here:
3/ pg_stat_get_backend_aio() and pg_stat_get_backend_io() correlation
postgres=# SELECT executed_sync, executed_async FROM pg_stat_get_backend_aio(pg_backend_pid());
executed_sync | executed_async
---------------+----------------
46 | 3088
(1 row)
postgres=# SELECT object, context, reads, read_bytes FROM pg_stat_get_backend_io(pg_backend_pid());
object | context | reads | read_bytes
---------------+-----------+-------+------------
relation | bulkread | 3088 | 401580032
relation | bulkwrite | 0 | 0
relation | init | 0 | 0
relation | normal | 46 | 376832
relation | vacuum | 0 | 0
temp relation | normal | 0 | 0
wal | init | |
wal | normal | 0 | 0
(8 rows)
We can see that the "executed_sync" matches the reads "normal" context and that
the "executed_async" matches the reads "bulkread" context.
4/ io_uring and multiple backends
postgres=# SELECT a.pid,
(pg_stat_get_backend_aio(a.pid)).completed_other
FROM pg_stat_activity a
WHERE a.backend_type = 'client backend';
pid | completed_other
---------+-----------------
1911889 | 245
1911892 | 511
1911912 | 147
1911933 | 161
(4 rows)
We can see that the backends completed AIO on behalf of other backends, which
makes fully sense in io_uring mode.
5/ io_max_concurrency = 4
postgres=# SELECT started, handle_waits FROM pg_stat_get_backend_aio(pg_backend_pid());
started | handle_waits
---------+--------------
3139 | 3026
(1 row)
We can see that the backend had to wait for free AIO handles on 96% of its IOs.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 225+ messages in thread
* Re: Add per-backend AIO statistics
@ 2026-07-08 06:52 Michael Paquier <[email protected]>
parent: Bertrand Drouvot <[email protected]>
1 sibling, 2 replies; 225+ messages in thread
From: Michael Paquier @ 2026-07-08 06:52 UTC (permalink / raw)
To: Bertrand Drouvot <[email protected]>; +Cc: [email protected]; Andres Freund <[email protected]>
On Tue, Jul 07, 2026 at 11:02:03AM +0000, Bertrand Drouvot wrote:
> 1/ pg_aios that lists all AIO handles that are currently in use. That shows
> what's happening right now, but not what has happened.
>
> 2/ pg_stat_get_backend_io() that shows how much IO was done, but not how it
> was done. There's no way to see whether IOs ran synchronously or
> asynchronously, whether a backend was stalling on handle exhaustion, or how
> completions are distributed across backends.
While the information may be useful, one thing that sounds very
important to me is how this impacts workloads by default.
Andres is usually able to catch bottlenecks that everybody else is
unable to see, so perhaps checking with him the location of these
extra function calls would be a good first step. Your proposal goes
down to pgaio_io_stage(), pgaio_io_process_completion() and
pgaio_submit_staged() to track these counter increments.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 225+ messages in thread
* Re: Add per-backend AIO statistics
@ 2026-07-08 08:15 Bertrand Drouvot <[email protected]>
parent: Michael Paquier <[email protected]>
1 sibling, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-07-08 08:15 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: [email protected]; Andres Freund <[email protected]>
Hi,
On Wed, Jul 08, 2026 at 03:52:20PM +0900, Michael Paquier wrote:
> On Tue, Jul 07, 2026 at 11:02:03AM +0000, Bertrand Drouvot wrote:
> > 1/ pg_aios that lists all AIO handles that are currently in use. That shows
> > what's happening right now, but not what has happened.
> >
> > 2/ pg_stat_get_backend_io() that shows how much IO was done, but not how it
> > was done. There's no way to see whether IOs ran synchronously or
> > asynchronously, whether a backend was stalling on handle exhaustion, or how
> > completions are distributed across backends.
>
> While the information may be useful,
Thanks for looking at it!
> Andres is usually able to catch bottlenecks that everybody else is
> unable to see, so perhaps checking with him the location of these
> extra function calls would be a good first step. Your proposal goes
> down to pgaio_io_stage(), pgaio_io_process_completion() and
> pgaio_submit_staged() to track these counter increments.
yeah, and also to 1/ confirm that I did understand this area of the AIO code
correctly and 2/ see if other counters could make sense.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 225+ messages in thread
* Re: Add per-backend AIO statistics
@ 2026-07-08 18:08 Andres Freund <[email protected]>
parent: Michael Paquier <[email protected]>
1 sibling, 1 reply; 225+ messages in thread
From: Andres Freund @ 2026-07-08 18:08 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; [email protected]
Hi,
On 2026-07-08 15:52:20 +0900, Michael Paquier wrote:
> On Tue, Jul 07, 2026 at 11:02:03AM +0000, Bertrand Drouvot wrote:
> > 1/ pg_aios that lists all AIO handles that are currently in use. That shows
> > what's happening right now, but not what has happened.
> >
> > 2/ pg_stat_get_backend_io() that shows how much IO was done, but not how it
> > was done. There's no way to see whether IOs ran synchronously or
> > asynchronously, whether a backend was stalling on handle exhaustion, or how
> > completions are distributed across backends.
>
> While the information may be useful, one thing that sounds very
> important to me is how this impacts workloads by default.
> Andres is usually able to catch bottlenecks that everybody else is
> unable to see, so perhaps checking with him the location of these
> extra function calls would be a good first step. Your proposal goes
> down to pgaio_io_stage(), pgaio_io_process_completion() and
> pgaio_submit_staged() to track these counter increments.
I think the overhead might be ok, but I am rather doubtful that all of this
information is actually useful. You're adding quite a few counters for each
IO, do we actually need that?
E.g. what do we gain from counting:
- started (if you want to see the number of IOs that are in progress,
cumulative stats are the wrong tool)
- executed_async (that's just the number of IOs minus executed_sync)
- completed_self (that's just the number of IOs minus executed_other)
Separately, I'm doubtful it makes sense to have only per-backend stats for
this. I think you'd almost always want the stats for exited backend
(e.g. parallel workers) too.
Unfortunately I'm pretty doubtful that pgstat_backend.c is the right
architectural direction. It'll just end up implementing all kinds of stats,
since we'll incrementally want more and more per-backend stats. I think what
we'd want is rather something where for each applicable stats kind we have a
shared counter for all exited backends and then per-backend counters for live
backends, with helpers to aggregate the exited + live stats to a total.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 225+ messages in thread
* Re: Add per-backend AIO statistics
@ 2026-07-09 04:19 Bertrand Drouvot <[email protected]>
parent: Andres Freund <[email protected]>
0 siblings, 1 reply; 225+ messages in thread
From: Bertrand Drouvot @ 2026-07-09 04:19 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Michael Paquier <[email protected]>; [email protected]
Hi,
On Wed, Jul 08, 2026 at 02:08:00PM -0400, Andres Freund wrote:
> Hi,
>
> On 2026-07-08 15:52:20 +0900, Michael Paquier wrote:
> > On Tue, Jul 07, 2026 at 11:02:03AM +0000, Bertrand Drouvot wrote:
> > > 1/ pg_aios that lists all AIO handles that are currently in use. That shows
> > > what's happening right now, but not what has happened.
> > >
> > > 2/ pg_stat_get_backend_io() that shows how much IO was done, but not how it
> > > was done. There's no way to see whether IOs ran synchronously or
> > > asynchronously, whether a backend was stalling on handle exhaustion, or how
> > > completions are distributed across backends.
> >
> > While the information may be useful, one thing that sounds very
> > important to me is how this impacts workloads by default.
>
>
> > Andres is usually able to catch bottlenecks that everybody else is
> > unable to see, so perhaps checking with him the location of these
> > extra function calls would be a good first step. Your proposal goes
> > down to pgaio_io_stage(), pgaio_io_process_completion() and
> > pgaio_submit_staged() to track these counter increments.
>
> I think the overhead might be ok,
Thanks for the feedback.
> but I am rather doubtful that all of this
> information is actually useful. You're adding quite a few counters for each
> IO, do we actually need that?
>
> E.g. what do we gain from counting:
> - started (if you want to see the number of IOs that are in progress,
> cumulative stats are the wrong tool)
> - executed_async (that's just the number of IOs minus executed_sync)
> - completed_self (that's just the number of IOs minus executed_other)
Yeah, we can remove some fields (as they're derivable).
> Separately, I'm doubtful it makes sense to have only per-backend stats for
> this. I think you'd almost always want the stats for exited backend
> (e.g. parallel workers) too.
Indeed, adding a global view would capture their activity.
> Unfortunately I'm pretty doubtful that pgstat_backend.c is the right
> architectural direction. It'll just end up implementing all kinds of stats,
> since we'll incrementally want more and more per-backend stats. I think what
> we'd want is rather something where for each applicable stats kind we have a
> shared counter for all exited backends and then per-backend counters for live
> backends, with helpers to aggregate the exited + live stats to a total.
That's a very nice proposal that would avoid the double counting. OTOH, that's
also a major re-design that would benefit all existing per-backend stats kinds.
I can see 2 options:
1/
step 1: Implement per-backend AIO stats (like proposed taking into account your
remark about useless, derivable fields) + a global view.
step 2: work on the re-design
2/
step 1: work on the redesign
step 2: Add AIO stats based on the re-design
The pros of 1/ is that step 1 would most probably land in 20, providing more user
visibility (+ it could be used or improved during the AIO write project). Step 2
is a much larger project that might not land in 20.
The cons, would be double counting (as there is no need to try to implement
something like [1] as we are going to re-design anyway).
I'll be tempted to vote for 1/ to provide faster added value. What do you (Andres,
Michael) think?
[1]: https://postgr.es/m/[email protected]
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 225+ messages in thread
* Re: Add per-backend AIO statistics
@ 2026-07-10 04:56 Bertrand Drouvot <[email protected]>
parent: Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 225+ messages in thread
From: Bertrand Drouvot @ 2026-07-10 04:56 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Michael Paquier <[email protected]>; [email protected]
Hi,
On Thu, Jul 09, 2026 at 04:19:26AM +0000, Bertrand Drouvot wrote:
> Hi,
>
> On Wed, Jul 08, 2026 at 02:08:00PM -0400, Andres Freund wrote:
>
> > Unfortunately I'm pretty doubtful that pgstat_backend.c is the right
> > architectural direction. It'll just end up implementing all kinds of stats,
> > since we'll incrementally want more and more per-backend stats. I think what
> > we'd want is rather something where for each applicable stats kind we have a
> > shared counter for all exited backends and then per-backend counters for live
> > backends, with helpers to aggregate the exited + live stats to a total.
>
> That's a very nice proposal that would avoid the double counting. OTOH, that's
> also a major re-design that would benefit all existing per-backend stats kinds.
>
> I can see 2 options:
>
> 1/
>
> step 1: Implement per-backend AIO stats (like proposed taking into account your
> remark about useless, derivable fields) + a global view.
> step 2: work on the re-design
>
> 2/
>
> step 1: work on the redesign
> step 2: Add AIO stats based on the re-design
>
> The pros of 1/ is that step 1 would most probably land in 20, providing more user
> visibility (+ it could be used or improved during the AIO write project). Step 2
> is a much larger project that might not land in 20.
>
> The cons, would be double counting (as there is no need to try to implement
> something like [1] as we are going to re-design anyway).
>
> I'll be tempted to vote for 1/ to provide faster added value. What do you (Andres,
> Michael) think?
Actually, there is no rush to merge the per-backend AIO stats (we still have
plenty of time for 20). So let's try option 2 and implement the new design first
and see where it goes. I'll create a dedicated thread once ready.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 225+ messages in thread
end of thread, other threads:[~2026-07-10 04:56 UTC | newest]
Thread overview: 225+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-12-21 06:19 [PATCH v25 5/9] Row pattern recognition patch (executor). Tatsuo Ishii <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-06-11 09:45 [PATCH v1] Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-07-07 11:02 Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-07-08 06:00 ` Re: Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-07-08 06:52 ` Re: Add per-backend AIO statistics Michael Paquier <[email protected]>
2026-07-08 08:15 ` Re: Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-07-08 18:08 ` Re: Add per-backend AIO statistics Andres Freund <[email protected]>
2026-07-09 04:19 ` Re: Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
2026-07-10 04:56 ` Re: Add per-backend AIO statistics Bertrand Drouvot <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox