v1-0001-pgstat-support-custom-serialization-files-and-cal.patch
application/octet-stream
Filename: v1-0001-pgstat-support-custom-serialization-files-and-cal.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v1-0001
Subject: pgstat: support custom serialization files and callbacks
| File | + | − |
|---|---|---|
| src/backend/utils/activity/pgstat.c | 319 | 93 |
| src/include/utils/pgstat_internal.h | 21 | 0 |
From 9dbb19a857862168d6168377389dd86a595962c9 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 10 Nov 2025 00:03:41 -0600
Subject: [PATCH v1 1/2] pgstat: support custom serialization files and
callbacks
Allow custom statistics kinds to serialize and deserialize extra
per-entry data, supporting kinds with variable auxiliary data that
cannot fit in the fixed shared-memory layout.
Add optional to_serialized_extra and from_serialized_extra callbacks
to PgStat_KindInfo, along with num_serialized_extra_files. Both
callbacks must be provided together, and num_serialized_extra_files
must be > 0.
The to_serialized_extra callback writes extra data to disk, and
from_serialized_extra reads it back into the entry. The callbacks
receive the entry key, header, core stats file descriptor, and a list
of extra file descriptors, up to the number specified by
num_serialized_extra_files.
Introduce PgStat_SerializeFiles to track temporary and permanent file
paths and descriptors. pgstat_allocate_files opens all files for read
or write, and pgstat_cleanup_files centralizes cleanup and unlinking.
Rename write_chunk/read_chunk helpers to pgstat_write_chunk and
pgstat_read_chunk, along with the *_s convenience macros, and make
these routines globally available for use by plug-ins.
---
src/backend/utils/activity/pgstat.c | 412 +++++++++++++++++++++-------
src/include/utils/pgstat_internal.h | 21 ++
2 files changed, 340 insertions(+), 93 deletions(-)
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 7ef06150df7..a5d0a475db0 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -154,6 +154,26 @@ typedef struct PgStat_SnapshotEntry
void *data; /* the stats data itself */
} PgStat_SnapshotEntry;
+/*
+ * Struct used to track the set of serialization files managed by the
+ * pgstat infrastructure. This includes both the core statistics file and
+ * any additional files defined by the stats kind. The core file must be
+ * listed at index 0.
+ *
+ * The structure maintains three parallel lists: temporary file paths,
+ * statistics file paths, and their corresponding file descriptors.
+ *
+ * Custom callbacks are provided the list of file desciptors that
+ * belong to their stats kind.
+ */
+typedef struct PgStat_SerializeFiles
+{
+ char **tmpfiles;
+ char **statfiles;
+ FILE **fd;
+ int num_files;
+} PgStat_SerializeFiles;
+
/* ----------
* Backend-local Hash Table Definitions
@@ -1525,6 +1545,34 @@ pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info)
errdetail("Existing cumulative statistics with ID %u has the same name.", existing_kind)));
}
+ /*
+ * Ensure that both serialization and deserialization callbacks are
+ * registered together or not at all, and only for custom stats with a
+ * variable number of extra files.
+ */
+ if ((kind_info->to_serialized_extra && !kind_info->from_serialized_extra) ||
+ (!kind_info->to_serialized_extra && kind_info->from_serialized_extra))
+ {
+ ereport(ERROR,
+ (errmsg("could not register custom cumulative statistics \"%s\" with ID %u",
+ kind_info->name, kind),
+ errdetail("Both to_serialized_extra and from_serialized_extra must be provided.")));
+
+ }
+
+ /*
+ * If extra serialization callbacks are set, num_serialized_extra_files
+ * must be greater than 0.
+ */
+ if (kind_info->to_serialized_extra &&
+ kind_info->num_serialized_extra_files < 1)
+ {
+ ereport(ERROR,
+ (errmsg("could not register custom cumulative statistics \"%s\" with ID %u",
+ kind_info->name, kind),
+ errdetail("Extra serialization callbacks were specified, but num_serialized_extra_files is zero")));
+ }
+
/* Register it */
pgstat_kind_custom_infos[idx] = kind_info;
ereport(LOG,
@@ -1552,8 +1600,8 @@ pgstat_assert_is_up(void)
*/
/* helpers for pgstat_write_statsfile() */
-static void
-write_chunk(FILE *fpout, void *ptr, size_t len)
+void
+pgstat_write_chunk(FILE *fpout, void *ptr, size_t len)
{
int rc;
@@ -1563,7 +1611,178 @@ write_chunk(FILE *fpout, void *ptr, size_t len)
(void) rc;
}
-#define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr))
+/* helpers for pgstat_read_statsfile() */
+bool
+pgstat_read_chunk(FILE *fpin, void *ptr, size_t len)
+{
+ return fread(ptr, 1, len, fpin) == len;
+}
+
+/*
+ * Close and remove all files recorded in a PgStat_SerializeFiles array.
+ * Depending on the is_temporary flag, this will remove either the temporary
+ * or the permanent filenames associated with each file descriptor.
+ *
+ * Errors encountered while closing files are logged, but cleanup continues
+ * for all remaining files. This function assumes the structure passed in
+ * was allocated by pgstat_allocate_files() and contains valid file descriptors
+ * for each opened stats file.
+ *
+ * NB: The array includes an extra slot at files[PGSTAT_KIND_MIN-1] for the
+ * core stats file. The loop below iterates from 0 to PGSTAT_KIND_CUSTOM_SIZE,
+ * processing all allocated slots, including the extra one.
+ */
+static void
+pgstat_cleanup_files(PgStat_SerializeFiles *files, bool is_temporary)
+{
+ for (int i = 0; i < PGSTAT_KIND_CUSTOM_SIZE + 1; i++)
+ {
+ for (int j = 0; j < files[i].num_files; j++)
+ {
+ FILE *fd = files[i].fd[j];
+
+ const char *filename = is_temporary ?
+ files[i].tmpfiles[j] :
+ files[i].statfiles[j];
+ const char *type_str = is_temporary ? "temporary" : "permanent";
+
+ elog(DEBUG2, "removing %s stats file \"%s\"", type_str, filename);
+
+ if (fd && FreeFile(fd) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not close %s statistics file \"%s\": %m",
+ type_str, filename)));
+ }
+
+ unlink(filename);
+
+ /* free per-file strings */
+ pfree(files[i].tmpfiles[j]);
+ pfree(files[i].statfiles[j]);
+ }
+
+ /* free each file arrays */
+ if (files[i].fd)
+ pfree(files[i].fd);
+
+ if (files[i].tmpfiles)
+ pfree(files[i].tmpfiles);
+
+ if (files[i].statfiles)
+ pfree(files[i].statfiles);
+ }
+
+ /* finally free top-level array */
+ pfree(files);
+}
+
+/*
+ * Allocate and open all statistics serialization files for reading or writing.
+ *
+ * The returned array contains the core permanent stats file at index 0,
+ * followed by any per-kind extra files at indices PGSTAT_KIND_CUSTOM_MIN
+ * to PGSTAT_KIND_CUSTOM_MAX.
+ *
+ * In write mode, temporary files are opened. If any file cannot be opened,
+ * all partially created temporary files are removed using pgstat_cleanup_files(),
+ * and NULL is returned.
+ *
+ * In read mode, permanent files are opened. If the core file cannot be opened,
+ * all stats are reset and NULL is returned. If a per-kind extra file cannot be
+ * opened, we proceed to the next extra file.
+ *
+ * Returns an array of PgStat_SerializeFiles on success, or NULL on failure
+ * after cleanup or stats reset as described above.
+ *
+ * NB: pgstat_cleanup_files is responsible for pfree'ing the PgStat_SerializeFiles
+ * array.
+ */
+static PgStat_SerializeFiles *
+pgstat_allocate_files(bool is_read)
+{
+ const char *mode = is_read ? PG_BINARY_R : PG_BINARY_W;
+ const char *action = is_read ? "reading" : "writing";
+ const char *core_path;
+ PgStat_SerializeFiles *core;
+ PgStat_SerializeFiles *files = palloc0(sizeof(PgStat_SerializeFiles) * (PGSTAT_KIND_CUSTOM_SIZE + 1));
+
+ /* --- Core pgstat file setup --- */
+ core = &files[PGSTAT_KIND_MIN - 1];
+ core->num_files = 1;
+ core->tmpfiles = palloc(sizeof(char *));
+ core->statfiles = palloc(sizeof(char *));
+ core->fd = palloc(sizeof(FILE *));
+
+ core->tmpfiles[0] = pstrdup(PGSTAT_STAT_PERMANENT_TMPFILE);
+ core->statfiles[0] = pstrdup(PGSTAT_STAT_PERMANENT_FILENAME);
+
+ elog(DEBUG2, "%s stats file \"%s\"", action, core->statfiles[0]);
+
+ core_path = is_read ? core->statfiles[0] : core->tmpfiles[0];
+ core->fd[0] = AllocateFile(core_path, mode);
+ if (core->fd[0] == NULL)
+ {
+ if (!is_read || errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not open %s statistics file \"%s\": %m",
+ is_read ? "permanent" : "temporary", core_path)));
+
+ if (!is_read)
+ pgstat_cleanup_files(files, true);
+ else
+ pgstat_reset_after_failure();
+
+ return NULL;
+ }
+
+ /* --- Per-kind extra pgstat files --- */
+ for (PgStat_Kind kind = PGSTAT_KIND_CUSTOM_MIN; kind <= PGSTAT_KIND_CUSTOM_MAX; kind++)
+ {
+ int nfiles;
+ int index = (kind - PGSTAT_KIND_CUSTOM_MIN) + 1;
+ PgStat_SerializeFiles *extra;
+ const PgStat_KindInfo *info = pgstat_get_kind_info(kind);
+
+ if (!info || info->num_serialized_extra_files < 1)
+ continue;
+
+ nfiles = info->num_serialized_extra_files;
+ extra = &files[index];
+
+ extra->num_files = nfiles;
+ extra->tmpfiles = palloc(sizeof(char *) * nfiles);
+ extra->statfiles = palloc(sizeof(char *) * nfiles);
+ extra->fd = palloc(sizeof(FILE *) * nfiles);
+
+ for (int i = 0; i < nfiles; i++)
+ {
+ const char *path;
+
+ extra->tmpfiles[i] = psprintf("%s/pgstat.%d.%d.tmp",
+ PGSTAT_STAT_PERMANENT_DIRECTORY, kind, i);
+ extra->statfiles[i] = psprintf("%s/pgstat.%d.%d.stat",
+ PGSTAT_STAT_PERMANENT_DIRECTORY, kind, i);
+
+ path = is_read ? extra->statfiles[i] : extra->tmpfiles[i];
+ elog(DEBUG2, "%s stats file \"%s\"", action, extra->statfiles[i]);
+
+ extra->fd[i] = AllocateFile(path, mode);
+ if (extra->fd[i] == NULL)
+ {
+ if (!is_read || errno != ENOENT)
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not open %s statistics file \"%s\": %m",
+ is_read ? "permanent" : "temporary ", path)));
+ }
+ }
+ }
+
+ return files;
+}
/*
* This function is called in the last process that is accessing the shared
@@ -1574,10 +1793,9 @@ pgstat_write_statsfile(void)
{
FILE *fpout;
int32 format_id;
- const char *tmpfile = PGSTAT_STAT_PERMANENT_TMPFILE;
- const char *statfile = PGSTAT_STAT_PERMANENT_FILENAME;
dshash_seq_status hstat;
PgStatShared_HashEntry *ps;
+ PgStat_SerializeFiles *files = NULL;
pgstat_assert_is_up();
@@ -1587,26 +1805,17 @@ pgstat_write_statsfile(void)
/* we're shutting down, so it's ok to just override this */
pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_NONE;
- elog(DEBUG2, "writing stats file \"%s\"", statfile);
-
- /*
- * Open the statistics temp file to write out the current values.
- */
- fpout = AllocateFile(tmpfile, PG_BINARY_W);
- if (fpout == NULL)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not open temporary statistics file \"%s\": %m",
- tmpfile)));
+ files = pgstat_allocate_files(false);
+ if (files == NULL)
return;
- }
+
+ fpout = files[PGSTAT_KIND_MIN - 1].fd[0];
/*
* Write the file header --- currently just a format ID.
*/
format_id = PGSTAT_FILE_FORMAT_ID;
- write_chunk_s(fpout, &format_id);
+ pgstat_write_chunk_s(fpout, &format_id);
/* Write various stats structs for fixed number of objects */
for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++)
@@ -1631,8 +1840,8 @@ pgstat_write_statsfile(void)
ptr = pgStatLocal.snapshot.custom_data[kind - PGSTAT_KIND_CUSTOM_MIN];
fputc(PGSTAT_FILE_ENTRY_FIXED, fpout);
- write_chunk_s(fpout, &kind);
- write_chunk(fpout, ptr, info->shared_data_len);
+ pgstat_write_chunk_s(fpout, &kind);
+ pgstat_write_chunk(fpout, ptr, info->shared_data_len);
}
/*
@@ -1686,7 +1895,7 @@ pgstat_write_statsfile(void)
{
/* normal stats entry, identified by PgStat_HashKey */
fputc(PGSTAT_FILE_ENTRY_HASH, fpout);
- write_chunk_s(fpout, &ps->key);
+ pgstat_write_chunk_s(fpout, &ps->key);
}
else
{
@@ -1696,57 +1905,73 @@ pgstat_write_statsfile(void)
kind_info->to_serialized_name(&ps->key, shstats, &name);
fputc(PGSTAT_FILE_ENTRY_NAME, fpout);
- write_chunk_s(fpout, &ps->key.kind);
- write_chunk_s(fpout, &name);
+ pgstat_write_chunk_s(fpout, &ps->key.kind);
+ pgstat_write_chunk_s(fpout, &name);
+ }
+
+ /* A plug-in is saving extra data */
+ if (kind_info->to_serialized_extra)
+ {
+ int index = (ps->key.kind - PGSTAT_KIND_CUSTOM_MIN) + 1;
+
+ Assert(files[index].fd);
+
+ kind_info->to_serialized_extra(&ps->key, shstats, fpout, files[index].fd);
}
/* Write except the header part of the entry */
- write_chunk(fpout,
- pgstat_get_entry_data(ps->key.kind, shstats),
- pgstat_get_entry_len(ps->key.kind));
+ pgstat_write_chunk(fpout,
+ pgstat_get_entry_data(ps->key.kind, shstats),
+ pgstat_get_entry_len(ps->key.kind));
+
}
dshash_seq_term(&hstat);
/*
* No more output to be done. Close the temp file and replace the old
* pgstat.stat with it. The ferror() check replaces testing for error
- * after each individual fputc or fwrite (in write_chunk()) above.
+ * after each individual fputc or fwrite (in pgstat_write_chunk()) above.
*/
fputc(PGSTAT_FILE_ENTRY_END, fpout);
- if (ferror(fpout))
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not write temporary statistics file \"%s\": %m",
- tmpfile)));
- FreeFile(fpout);
- unlink(tmpfile);
- }
- else if (FreeFile(fpout) < 0)
- {
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not close temporary statistics file \"%s\": %m",
- tmpfile)));
- unlink(tmpfile);
- }
- else if (durable_rename(tmpfile, statfile, LOG) < 0)
+ for (int i = 0; i < PGSTAT_KIND_CUSTOM_SIZE + 1; i++)
{
- /* durable_rename already emitted log message */
- unlink(tmpfile);
+ for (int j = 0; j < files[i].num_files; j++)
+ {
+ const char *tmpfile;
+ const char *statfile;
+ FILE *fd = NULL;
+
+ tmpfile = files[i].tmpfiles[j];
+ statfile = files[i].statfiles[j];
+ fd = files[i].fd[j];
+
+ if (ferror(fd))
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not write temporary statistics file \"%s\": %m",
+ tmpfile)));
+ FreeFile(fd);
+ unlink(tmpfile);
+ }
+ else if (FreeFile(fd) < 0)
+ {
+ ereport(LOG,
+ (errcode_for_file_access(),
+ errmsg("could not close temporary statistics file \"%s\": %m",
+ tmpfile)));
+ unlink(tmpfile);
+ }
+ else if (durable_rename(tmpfile, statfile, LOG) < 0)
+ {
+ /* durable_rename already emitted log message */
+ unlink(tmpfile);
+ }
+ }
}
}
-/* helpers for pgstat_read_statsfile() */
-static bool
-read_chunk(FILE *fpin, void *ptr, size_t len)
-{
- return fread(ptr, 1, len, fpin) == len;
-}
-
-#define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr))
-
/*
* Reads in existing statistics file into memory.
*
@@ -1761,36 +1986,21 @@ pgstat_read_statsfile(void)
bool found;
const char *statfile = PGSTAT_STAT_PERMANENT_FILENAME;
PgStat_ShmemControl *shmem = pgStatLocal.shmem;
+ PgStat_SerializeFiles *files = NULL;
/* shouldn't be called from postmaster */
Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
- elog(DEBUG2, "reading stats file \"%s\"", statfile);
-
- /*
- * Try to open the stats file. If it doesn't exist, the backends simply
- * returns zero for anything and statistics simply starts from scratch
- * with empty counters.
- *
- * ENOENT is a possibility if stats collection was previously disabled or
- * has not yet written the stats file for the first time. Any other
- * failure condition is suspicious.
- */
- if ((fpin = AllocateFile(statfile, PG_BINARY_R)) == NULL)
- {
- if (errno != ENOENT)
- ereport(LOG,
- (errcode_for_file_access(),
- errmsg("could not open statistics file \"%s\": %m",
- statfile)));
- pgstat_reset_after_failure();
+ files = pgstat_allocate_files(true);
+ if (files == NULL)
return;
- }
+
+ fpin = files[PGSTAT_KIND_MIN - 1].fd[0];
/*
* Verify it's of the expected format.
*/
- if (!read_chunk_s(fpin, &format_id))
+ if (!pgstat_read_chunk_s(fpin, &format_id))
{
elog(WARNING, "could not read format ID");
goto error;
@@ -1820,7 +2030,7 @@ pgstat_read_statsfile(void)
char *ptr;
/* entry for fixed-numbered stats */
- if (!read_chunk_s(fpin, &kind))
+ if (!pgstat_read_chunk_s(fpin, &kind))
{
elog(WARNING, "could not read stats kind for entry of type %c", t);
goto error;
@@ -1860,7 +2070,7 @@ pgstat_read_statsfile(void)
info->shared_data_off;
}
- if (!read_chunk(fpin, ptr, info->shared_data_len))
+ if (!pgstat_read_chunk(fpin, ptr, info->shared_data_len))
{
elog(WARNING, "could not read data of stats kind %u for entry of type %c with size %u",
kind, t, info->shared_data_len);
@@ -1875,13 +2085,14 @@ pgstat_read_statsfile(void)
PgStat_HashKey key;
PgStatShared_HashEntry *p;
PgStatShared_Common *header;
+ const PgStat_KindInfo *kind_info = NULL;
CHECK_FOR_INTERRUPTS();
if (t == PGSTAT_FILE_ENTRY_HASH)
{
/* normal stats entry, identified by PgStat_HashKey */
- if (!read_chunk_s(fpin, &key))
+ if (!pgstat_read_chunk_s(fpin, &key))
{
elog(WARNING, "could not read key for entry of type %c", t);
goto error;
@@ -1894,8 +2105,8 @@ pgstat_read_statsfile(void)
key.objid, t);
goto error;
}
-
- if (!pgstat_get_kind_info(key.kind))
+ kind_info = pgstat_get_kind_info(key.kind);
+ if (!kind_info)
{
elog(WARNING, "could not find information of kind for entry %u/%u/%" PRIu64 " of type %c",
key.kind, key.dboid,
@@ -1906,16 +2117,15 @@ pgstat_read_statsfile(void)
else
{
/* stats entry identified by name on disk (e.g. slots) */
- const PgStat_KindInfo *kind_info = NULL;
PgStat_Kind kind;
NameData name;
- if (!read_chunk_s(fpin, &kind))
+ if (!pgstat_read_chunk_s(fpin, &kind))
{
elog(WARNING, "could not read stats kind for entry of type %c", t);
goto error;
}
- if (!read_chunk_s(fpin, &name))
+ if (!pgstat_read_chunk_s(fpin, &name))
{
elog(WARNING, "could not read name of stats kind %u for entry of type %c",
kind, t);
@@ -1990,9 +2200,9 @@ pgstat_read_statsfile(void)
key.objid, t);
}
- if (!read_chunk(fpin,
- pgstat_get_entry_data(key.kind, header),
- pgstat_get_entry_len(key.kind)))
+ if (!pgstat_read_chunk(fpin,
+ pgstat_get_entry_data(key.kind, header),
+ pgstat_get_entry_len(key.kind)))
{
elog(WARNING, "could not read data for entry %u/%u/%" PRIu64 " of type %c",
key.kind, key.dboid,
@@ -2000,6 +2210,25 @@ pgstat_read_statsfile(void)
goto error;
}
+ /*
+ * A plug-in is reading extra data. If reading fails, the
+ * corresponding file is closed, the error is logged, and
+ * processing continues.
+ */
+ if (kind_info->from_serialized_extra)
+ {
+ int index = (key.kind - PGSTAT_KIND_CUSTOM_MIN) + 1;
+
+ Assert(files[index].fd);
+
+ if (!kind_info->from_serialized_extra(&key, header, fpin, files[index].fd))
+ {
+ elog(WARNING, "could not read extra data for entry %u/%u/%" PRIu64 " of type %c",
+ key.kind, key.dboid,
+ key.objid, t);
+ }
+ }
+
break;
}
case PGSTAT_FILE_ENTRY_END:
@@ -2023,10 +2252,7 @@ pgstat_read_statsfile(void)
}
done:
- FreeFile(fpin);
-
- elog(DEBUG2, "removing permanent stats file \"%s\"", statfile);
- unlink(statfile);
+ pgstat_cleanup_files(files, false);
return;
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 4d2b8aa6081..be39c4518f8 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -303,6 +303,16 @@ typedef struct PgStat_KindInfo
const PgStatShared_Common *header, NameData *name);
bool (*from_serialized_name) (const NameData *name, PgStat_HashKey *key);
+ /*
+ * For custom, variable-numbered stats, serialize/deserialize extra data
+ * per entry. Optional.
+ */
+ bool (*from_serialized_extra) (PgStat_HashKey *key,
+ const PgStatShared_Common *header, FILE *statfile, FILE **extra_files);
+ void (*to_serialized_extra) (const PgStat_HashKey *key,
+ const PgStatShared_Common *header, FILE *statfile, FILE **extra_files);
+ int num_serialized_extra_files;
+
/*
* For fixed-numbered statistics: Initialize shared memory state.
*
@@ -984,4 +994,15 @@ pgstat_get_custom_snapshot_data(PgStat_Kind kind)
return pgStatLocal.snapshot.custom_data[idx];
}
+/* ------------------------------------------------------------
+ * reading and writing of on-disk stats file
+ * ------------------------------------------------------------
+ */
+
+/* helpers for pgstat_write_statsfile() */
+extern void pgstat_write_chunk(FILE *fpout, void *ptr, size_t len);
+extern bool pgstat_read_chunk(FILE *fpin, void *ptr, size_t len);
+#define pgstat_read_chunk_s(fpin, ptr) pgstat_read_chunk(fpin, ptr, sizeof(*ptr))
+#define pgstat_write_chunk_s(fpout, ptr) pgstat_write_chunk(fpout, ptr, sizeof(*ptr))
+
#endif /* PGSTAT_INTERNAL_H */
--
2.43.0