From 257882eb9d6ad6d2c12d0c5be19d972a7f4f6618 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Mon, 26 Feb 2024 20:17:40 +0100
Subject: [PATCH v9 1/2] Explain: Add SERIALIZE option

This option integrates with both MEMORY and TIMING, and is gated behind
ANALYZE.

EXPLAIN (SERIALIZE) allows analysis of the cost of actually serializing
the resultset, which usually can't be tested without actually consuming
the resultset on the client. As sending a resultset of gigabytes across
e.g. a VPN connection can be slow and expensive, this option increases
coverage of EXPLAIN and allows for further diagnostics in case of e.g.
attributes that are slow to deTOAST.

Future iterations may want to further instrument the deTOAST and ANALYZE
infrastructure to measure counts of deTOAST operations, but that is not
part of this patch.

Original patch by Stepan Rutz <stepan.rutz@gmx.de>, heavily modified by
Matthias van de Meent <boekewurm+postgres@gmail.com>
---
 src/backend/commands/explain.c        | 511 +++++++++++++++++++++++++-
 src/bin/psql/tab-complete.c           |   4 +-
 src/include/commands/explain.h        |   9 +
 src/test/regress/expected/explain.out |  57 ++-
 src/test/regress/sql/explain.sql      |  28 +-
 5 files changed, 602 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a9d5056af4..9b1f4b6ba1 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -20,6 +20,7 @@
 #include "commands/prepare.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
+#include "libpq/pqformat.h"
 #include "nodes/extensible.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -32,6 +33,7 @@
 #include "utils/guc_tables.h"
 #include "utils/json.h"
 #include "utils/lsyscache.h"
+#include "utils/memdebug.h"
 #include "utils/rel.h"
 #include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
@@ -46,6 +48,15 @@ ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
 /* Hook for plugins to get control in explain_get_index_name() */
 explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
 
+/* Instrumentation structures for EXPLAIN's SERIALIZE option */
+typedef struct ExplSerInstrumentation
+{
+	uint64				bytesSent;		/* # of bytes serialized */
+	instr_time			timeSpent;		/* time spent serializing */
+	MemoryContextCounters memory;		/* memory context counters */
+	MemoryContextCounters emptyMemory;		/* memory context counters */
+	ExplainSerializeFormat format;		/* serialization format */
+} ExplSerInstrumentation;
 
 /* OR-able flags for ExplainXMLTag() */
 #define X_OPENING 0
@@ -59,6 +70,8 @@ static void ExplainOneQuery(Query *query, int cursorOptions,
 							QueryEnvironment *queryEnv);
 static void ExplainPrintJIT(ExplainState *es, int jit_flags,
 							JitInstrumentation *ji);
+static void ExplainPrintSerialize(ExplainState *es,
+								  ExplSerInstrumentation *instr);
 static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
 							ExplainState *es);
 static double elapsed_time(instr_time *starttime);
@@ -155,7 +168,8 @@ static void ExplainJSONLineEnding(ExplainState *es);
 static void ExplainYAMLLineStarting(ExplainState *es);
 static void escape_yaml(StringInfo buf, const char *str);
 
-
+static DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es);
+static ExplSerInstrumentation GetSerializationMetrics(DestReceiver *dest);
 
 /*
  * ExplainQuery -
@@ -193,6 +207,34 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 			es->settings = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "generic_plan") == 0)
 			es->generic = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "serialize") == 0)
+		{
+			/* check the optional argument, if defined */
+			if (opt->arg)
+			{
+				char *p = defGetString(opt);
+				if (strcmp(p, "off") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_NONE;
+				else if (strcmp(p, "text") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_TEXT;
+				else if (strcmp(p, "binary") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_BINARY;
+				else
+					ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
+								opt->defname, p),
+						 parser_errposition(pstate, opt->location)));
+			}
+			else
+			{
+				/*
+				 * The default serialization mode when the option is specified
+				 * is 'text'.
+				 */
+				es->serialize = EXPLAIN_SERIALIZE_TEXT;
+			}
+		}
 		else if (strcmp(opt->defname, "timing") == 0)
 		{
 			timing_set = true;
@@ -247,6 +289,12 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("EXPLAIN option TIMING requires ANALYZE")));
 
+	/* check that serialize is used with EXPLAIN ANALYZE */
+	if (es->serialize != EXPLAIN_SERIALIZE_NONE && !es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option SERIALIZE requires ANALYZE")));
+
 	/* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
 	if (es->generic && es->analyze)
 		ereport(ERROR,
@@ -577,6 +625,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	double		totaltime = 0;
 	int			eflags;
 	int			instrument_option = 0;
+	ExplSerInstrumentation serializeMetrics = {0};
 
 	Assert(plannedstmt->commandType != CMD_UTILITY);
 
@@ -605,11 +654,15 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	UpdateActiveSnapshotCommandId();
 
 	/*
-	 * Normally we discard the query's output, but if explaining CREATE TABLE
-	 * AS, we'd better use the appropriate tuple receiver.
+	 * We discard the output if we have no use for it.
+	 * If we're explaining CREATE TABLE AS, we'd better use the appropriate
+	 * tuple receiver, and when we EXPLAIN (ANALYZE, SERIALIZE) we better set
+	 * up a serializing (but discarding) DestReceiver.
 	 */
 	if (into)
 		dest = CreateIntoRelDestReceiver(into);
+	else if (es->analyze && es->serialize != EXPLAIN_SERIALIZE_NONE)
+		dest = CreateExplainSerializeDestReceiver(es);
 	else
 		dest = None_Receiver;
 
@@ -648,6 +701,13 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		/* run cleanup too */
 		ExecutorFinish(queryDesc);
 
+		/* grab the metrics before we destroy the DestReceiver */
+		if (es->serialize)
+			serializeMetrics = GetSerializationMetrics(dest);
+
+		/* call the DestReceiver's destroy method even during explain */
+		dest->rDestroy(dest);
+
 		/* We can't run ExecutorEnd 'till we're done printing the stats... */
 		totaltime += elapsed_time(&starttime);
 	}
@@ -729,6 +789,10 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		ExplainPropertyFloat("Execution Time", "ms", 1000.0 * totaltime, 3,
 							 es);
 
+	/* print the info about serialization of data */
+	if (es->summary && es->analyze && es->serialize != EXPLAIN_SERIALIZE_NONE)
+		ExplainPrintSerialize(es, &serializeMetrics);
+
 	ExplainCloseGroup("Query", NULL, true, es);
 }
 
@@ -5193,3 +5257,444 @@ escape_yaml(StringInfo buf, const char *str)
 {
 	escape_json(buf, str);
 }
+
+
+/*
+ * Serializing DestReceiver functions
+ *
+ * EXPLAIN (ANALYZE) can fail to provide accurate results for some queries,
+ * which can usually be attributed to a lack of deTOASTing when the resultset
+ * isn't fully serialized, or other features usually only accessed in the
+ * DestReceiver functions. To measure the overhead of transferring the
+ * resulting dataset of a query, the SERIALIZE option is added, which can show
+ * and measure the relevant metrics available to a PostgreSQL server. This
+ * allows the measuring of server time spent on deTOASTing, serialization and
+ * copying of data.
+ * 
+ * However, this critically does not measure the network performance: All
+ * measured timings are about processes inside the database.
+ */
+
+/* an attribute info cached for each column */
+typedef struct SerializeAttrInfo
+{								/* Per-attribute information */
+	Oid			typoutput;		/* Oid for the type's text output fn */
+	Oid			typsend;		/* Oid for the type's binary output fn */
+	bool		typisvarlena;	/* is it varlena (ie possibly toastable)? */
+	int8		format;			/* text of binary, like pq wire protocol */
+	FmgrInfo	finfo;			/* Precomputed call info for output fn */
+} SerializeAttrInfo;
+
+typedef struct SerializeDestReceiver
+{
+	/* receiver for the tuples, that just serializes */
+	DestReceiver		destRecevier;
+	MemoryContext		memoryContext;
+	ExplainState	   *es;					/* this EXPLAIN-statement's ExplainState */
+	int8				format;				/* text of binary, like pq wire protocol */
+	TupleDesc			attrinfo;
+	int					nattrs;
+	StringInfoData		buf;				/* serialization buffer to hold temporary data */
+	ExplSerInstrumentation metrics;			/* metrics */
+	SerializeAttrInfo	*infos;				/* Cached info about each attr */
+} SerializeDestReceiver;
+
+/*
+ * Get the lookup info that the row-callback of the receiver needs. this code
+ * is similar to the code from printup.c except that it doesn't do any actual
+ * output.
+ */
+static void
+serialize_prepare_info(SerializeDestReceiver *receiver, TupleDesc typeinfo,
+					   int nattrs)
+{
+	/* get rid of any old data */
+	if (receiver->infos)
+		pfree(receiver->infos);
+	receiver->infos = NULL;
+
+	receiver->attrinfo = typeinfo;
+	receiver->nattrs = nattrs;
+	if (nattrs <= 0)
+		return;
+
+	receiver->infos = (SerializeAttrInfo *)
+		palloc0(nattrs * sizeof(SerializeAttrInfo));
+
+	for (int i = 0; i < nattrs; i++)
+	{
+		SerializeAttrInfo *info = &receiver->infos[i];
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+		info->format = receiver->format;
+
+		if (info->format == 0)
+		{
+			/* wire protocol format text */
+			getTypeOutputInfo(attr->atttypid,
+							  &info->typoutput,
+							  &info->typisvarlena);
+			fmgr_info(info->typoutput, &info->finfo);
+		}
+		else if (info->format == 1) 
+		{
+			/* wire protocol format binary */
+			getTypeBinaryOutputInfo(attr->atttypid,
+									&info->typsend,
+									&info->typisvarlena);
+			fmgr_info(info->typsend, &info->finfo);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unsupported format code: %d", info->format)));
+		}
+	}
+}
+
+
+
+/*
+ * serializeAnalyzeReceive - process tuples for EXPLAIN (SERIALIZE)
+ *
+ * This method receives the tuples/records during EXPLAIN (ANALYZE, SERIALIZE)
+ * and serializes them while measuring various things about that
+ * serialization, in a way that should be as close as possible to printtup.c
+ * without actually sending the data; thus capturing the overhead of
+ * deTOASTing and type's out/sendfuncs, which are not otherwise exercisable
+ * without actually hitting the network, thus increasing the number of paths
+ * you can exercise with EXPLAIN.
+ * 
+ * See also: printtup() in printtup.c, the older twin of this code.
+ */
+static bool
+serializeAnalyzeReceive(TupleTableSlot *slot, DestReceiver *self)
+{
+	TupleDesc		tupdesc;
+	MemoryContext	oldcontext;
+	SerializeDestReceiver *receiver = (SerializeDestReceiver*) self;
+	StringInfo		buf = &receiver->buf;
+	instr_time		start, end;
+
+	tupdesc  = slot->tts_tupleDescriptor;
+
+	/* only measure time if requested */
+	if (receiver->es->timing)
+		INSTR_TIME_SET_CURRENT(start);
+
+	/* Cache attribute infos and function oid if outdated */
+	if (receiver->attrinfo != tupdesc || receiver->nattrs != tupdesc->natts)
+		serialize_prepare_info(receiver, tupdesc, tupdesc->natts);
+
+	/* Fill all the slot's attributes, we can now use slot->tts_values
+	 * and its tts_isnull array which should be long enough even if added
+	 * a null-column to the table */
+	slot_getallattrs(slot);
+
+	oldcontext = MemoryContextSwitchTo(receiver->memoryContext);
+
+	/*
+	 * Note that we us an actual StringInfo buffer. This is to include the
+	 * cost of memory accesses and copy operations, reducing the number of
+	 * operations unique to the true printtup path vs the EXPLAIN (SERIALIZE)
+	 * path.
+	 */
+	pq_beginmessage_reuse(buf, 'D');
+	pq_sendint16(buf, receiver->nattrs);
+
+	/*
+	 * Iterate over all attributes of the tuple and invoke the output func
+	 * (or send function in case of a binary format). We'll completely ignore
+	 * the result. The MemoryContext is reset at the end of this per-tuple
+	 * callback anyhow.
+	 */
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		SerializeAttrInfo *thisState = receiver->infos + i;
+		Datum		attr = slot->tts_values[i];
+
+		if (slot->tts_isnull[i])
+		{
+			pq_sendint32(buf, -1);
+			continue;
+		}
+
+		/*
+		 * Here we catch undefined bytes in datums that are returned to the
+		 * client without hitting disk; see comments at the related check in
+		 * PageAddItem().  This test is most useful for uncompressed,
+		 * non-external datums, but we're quite likely to see such here when
+		 * testing new C functions.
+		 */
+		if (thisState->typisvarlena)
+			VALGRIND_CHECK_MEM_IS_DEFINED(DatumGetPointer(attr),
+										  VARSIZE_ANY(attr));
+
+		if (thisState->format == 0)
+		{
+			/* Text output */
+			char	   *outputstr;
+
+			outputstr = OutputFunctionCall(&thisState->finfo, attr);
+			pq_sendcountedtext(buf, outputstr, strlen(outputstr));
+		}
+		else
+		{
+			/* Binary output */
+			bytea	   *outputbytes;
+			Assert(thisState->format == 1);
+
+			outputbytes = SendFunctionCall(&thisState->finfo, attr);
+			pq_sendint32(buf, VARSIZE(outputbytes) - VARHDRSZ);
+			pq_sendbytes(buf, VARDATA(outputbytes),
+						 VARSIZE(outputbytes) - VARHDRSZ);
+		}
+	}
+
+	/* finalize the timers */
+	if (receiver->es->timing)
+	{
+		INSTR_TIME_SET_CURRENT(end);
+		INSTR_TIME_ACCUM_DIFF(receiver->metrics.timeSpent, end, start);
+	}
+
+	/*
+	 * Register the size of the packet we would've sent to the client. The
+	 * buffer will be dropped on the next iteration.
+	 */
+	receiver->metrics.bytesSent += buf->len;
+
+	/*
+	 * Now that we're done processing we profile memory usage, if that was
+	 * requested by the user.
+	 */
+	if (receiver->es->memory)
+	{
+		MemoryContextCounters counters;
+		MemoryContextMemConsumed(receiver->memoryContext, &counters);
+
+		/*
+		 * Note: Although the freespace counter can (and likely does!)
+		 * underflow, that won't be an issue for the printed results: this
+		 * will only add used memory if more total space was allocated for
+		 * the context due to excessive allocations, but will always increase
+		 * the difference between the total totalspace and freespace by the
+		 * amount of bytes allocated each iteration by underflowing the
+		 * freespace counter. As memory used = totalspace - freespace, a
+		 * negative value for freespace also adds to the used counter, even
+		 * if it may be meaningless (and even nonsense!) on its own.
+		 *
+		 * However, it was decided to do it this way, to not overwhelm the
+		 * user with stats of at least 8kiB of Memory Allocated per output
+		 * tuple when that memory was actually retained in the Memory Context.
+		 */
+		receiver->metrics.memory.totalspace +=
+			counters.totalspace - receiver->metrics.emptyMemory.totalspace;
+		receiver->metrics.memory.freespace +=
+			counters.freespace - receiver->metrics.emptyMemory.freespace;
+		receiver->metrics.memory.freechunks +=
+			counters.freechunks - receiver->metrics.emptyMemory.freechunks;
+		receiver->metrics.memory.nblocks +=
+			counters.nblocks - receiver->metrics.emptyMemory.nblocks;
+	}
+
+	/* cleanup and reset */
+	MemoryContextSwitchTo(oldcontext);
+	MemoryContextReset(receiver->memoryContext);
+
+	return true;
+}
+
+static void
+serializeAnalyzeStartup(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	SerializeDestReceiver *receiver = (SerializeDestReceiver*) self;
+	/* memory context for our work */
+	receiver->memoryContext = AllocSetContextCreate(CurrentMemoryContext,
+		"SerializeTupleReceive", ALLOCSET_DEFAULT_SIZES);
+
+	/* initialize various fields */
+	INSTR_TIME_SET_ZERO(receiver->metrics.timeSpent);
+	initStringInfo(&receiver->buf);
+
+	/*
+	 * We ensure our memory accounting is accurate by subtracting the memory
+	 * usage of the empty memory context from measurements, so that we don't
+	 * count these allocations every time we receive a tuple: we don't
+	 * re-allocate the memory context every iteration; we only reset it.
+	 */
+	if (receiver->es->memory)
+	{
+		MemoryContextMemConsumed(receiver->memoryContext,
+								 &receiver->metrics.emptyMemory);
+		/*
+		 * ... But do count this memory context's empty allocations once at
+		 * the start, so that using a memory context with lower base overhead
+		 * shows up in these metrics.
+		 */
+		receiver->metrics.memory = receiver->metrics.emptyMemory;
+	}
+
+	/*
+	 * Note that we don't actually serialize the RowDescriptor message here.
+	 * It is assumed that this has negligible overhead in the grand scheme of
+	 * things; but if so desired it can be updated without much issue.
+	 */
+
+	/* account for the attribute headers for send bytes */
+	receiver->metrics.bytesSent += 3; /* protocol message type and attribute-count */
+	for (int i = 0; i < typeinfo->natts; ++i)
+	{
+		Form_pg_attribute att = TupleDescAttr(typeinfo, i);
+		char	   *name = NameStr(att->attname);
+		Size		namelen = strlen(name);
+
+		/* convert from server encoding to client encoding if needed */
+		char *converted = pg_server_to_client(name, (int) namelen);
+
+		if (converted != name)
+		{
+			namelen = strlen(converted);
+			pfree(converted); /* don't leak it */
+		}
+
+		/* see printtup.h why we add 18 bytes here. These are the infos
+		 * needed for each attribute plus the attribute's name */
+		receiver->metrics.bytesSent += (int64) namelen + 1 + 18;
+	}
+}
+
+/*
+ * serializeAnalyzeShutdown - shut down the serializeAnalyze receiver
+ */
+static void
+serializeAnalyzeShutdown(DestReceiver *self)
+{
+	SerializeDestReceiver *receiver = (SerializeDestReceiver*) self;
+
+	if (receiver->infos)
+		pfree(receiver->infos);
+	receiver->infos = NULL;
+
+	if (receiver->buf.data)
+		pfree(receiver->buf.data);
+	receiver->buf.data = NULL;
+
+	if (receiver->memoryContext)
+		MemoryContextDelete(receiver->memoryContext);
+	receiver->memoryContext = NULL;
+}
+
+/*
+ * serializeAnalyzeShutdown - shut down the serializeAnalyze receiver
+ */
+static void
+serializeAnalyzeDestroy(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/* Build a DestReceiver with EXPLAIN (SERIALIZE) instrumentation. */
+static DestReceiver *
+CreateExplainSerializeDestReceiver(ExplainState *es)
+{
+	SerializeDestReceiver *self;
+
+	self = (SerializeDestReceiver*) palloc0(sizeof(SerializeDestReceiver));
+
+	self->destRecevier.receiveSlot = serializeAnalyzeReceive;
+	self->destRecevier.rStartup = serializeAnalyzeStartup;
+	self->destRecevier.rShutdown = serializeAnalyzeShutdown;
+	self->destRecevier.rDestroy = serializeAnalyzeDestroy;
+	self->destRecevier.mydest = DestNone;
+
+	switch (es->serialize)
+	{
+		case EXPLAIN_SERIALIZE_NONE:
+			Assert(false);
+			elog(ERROR, "Invalid explain serialization format code %d", es->serialize);
+			break;
+		case EXPLAIN_SERIALIZE_TEXT:
+			self->format = 0; /* wire protocol format text */
+			break;
+		case EXPLAIN_SERIALIZE_BINARY:
+			self->format = 1; /* wire protocol format binary */
+			break;
+	}
+
+	/* store the ExplainState, for easier access to various fields */
+	self->es = es;
+
+	self->metrics.format = es->serialize;
+
+	return (DestReceiver *) self;
+}
+
+static ExplSerInstrumentation
+GetSerializationMetrics(DestReceiver *dest)
+{
+	return ((SerializeDestReceiver*) dest)->metrics;
+}
+
+/* Print data for the SERIALIZE option */
+static void
+ExplainPrintSerialize(ExplainState *es, ExplSerInstrumentation *instr)
+{
+	char	   *format;
+	if (instr->format == EXPLAIN_SERIALIZE_TEXT)
+		format = "text";
+	else
+	{
+		/* We shouldn't get called for EXPLAIN_SERIALIZE_NONE */
+		Assert(instr->format == EXPLAIN_SERIALIZE_BINARY);
+		format = "binary";
+	}
+
+	ExplainOpenGroup("Serialization", "Serialization", true, es);
+
+	if (es->format == EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainIndentText(es);
+		appendStringInfoString(es->str, "Serialization:");
+		appendStringInfoChar(es->str, '\n');
+		es->indent++;
+		ExplainIndentText(es);
+
+		/* timing is optional */
+		if (es->timing)
+			appendStringInfo(es->str, "Serialize: time=%.3f ms  produced=%lld bytes  format=%s",
+							 1000.0 * INSTR_TIME_GET_DOUBLE(instr->timeSpent),
+							 (long long) instr->bytesSent,
+							 format);
+		else
+			appendStringInfo(es->str, "Serialize: produced=%lld bytes  format=%s",
+							 (long long) instr->bytesSent,
+							 format);
+
+		appendStringInfoChar(es->str, '\n');
+
+		/* output memory stats, if applicable */
+		if (es->memory)
+			show_memory_counters(es, &instr->memory);
+		es->indent--;
+	}
+	else
+	{
+		if (es->timing)
+		{
+			ExplainPropertyFloat("Time", "ms",
+								 1000.0 * INSTR_TIME_GET_DOUBLE(instr->timeSpent),
+								 3, es);
+		}
+
+		ExplainPropertyUInteger("Produced", "bytes",
+								instr->bytesSent, es);
+		ExplainPropertyText("Format", format, es);
+
+		if (es->memory)
+			show_memory_counters(es, &instr->memory);
+	}
+
+	ExplainCloseGroup("Serialization", "Serialization", true, es);
+}
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 73133ce735..822d65b71e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3849,9 +3849,11 @@ psql_completion(const char *text, int start, int end)
 		 */
 		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
 			COMPLETE_WITH("ANALYZE", "VERBOSE", "COSTS", "SETTINGS", "GENERIC_PLAN",
-						  "BUFFERS", "WAL", "TIMING", "SUMMARY", "FORMAT");
+						  "BUFFERS", "SERIALIZE", "WAL", "TIMING", "SUMMARY", "FORMAT");
 		else if (TailMatches("ANALYZE|VERBOSE|COSTS|SETTINGS|GENERIC_PLAN|BUFFERS|WAL|TIMING|SUMMARY"))
 			COMPLETE_WITH("ON", "OFF");
+		else if (TailMatches("SERIALIZE"))
+			COMPLETE_WITH("NONE", "TEXT", "BINARY");
 		else if (TailMatches("FORMAT"))
 			COMPLETE_WITH("TEXT", "XML", "JSON", "YAML");
 	}
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index cf195f1359..b4bd6a2fcf 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -25,6 +25,13 @@ typedef enum ExplainFormat
 	EXPLAIN_FORMAT_YAML,
 } ExplainFormat;
 
+typedef enum ExplainSerializeFormat
+{
+	EXPLAIN_SERIALIZE_NONE,
+	EXPLAIN_SERIALIZE_TEXT,
+	EXPLAIN_SERIALIZE_BINARY,
+} ExplainSerializeFormat;
+
 typedef struct ExplainWorkersState
 {
 	int			num_workers;	/* # of worker processes the plan used */
@@ -48,6 +55,8 @@ typedef struct ExplainState
 	bool		memory;			/* print planner's memory usage information */
 	bool		settings;		/* print modified settings */
 	bool		generic;		/* generate a generic plan */
+	ExplainSerializeFormat serialize; /* do serialization (in ANALZYE) */
+
 	ExplainFormat format;		/* output format */
 	/* state for output formatting --- not reset for each new plan tree */
 	int			indent;			/* current indentation level */
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 1299ee79ad..ab39c75606 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -135,7 +135,7 @@ select explain_filter('explain (analyze, buffers, format xml) select * from int8
  </explain>
 (1 row)
 
-select explain_filter('explain (analyze, buffers, format yaml) select * from int8_tbl i8');
+select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8');
         explain_filter         
 -------------------------------
  - Plan:                      +
@@ -175,7 +175,11 @@ select explain_filter('explain (analyze, buffers, format yaml) select * from int
      Temp Written Blocks: N   +
    Planning Time: N.N         +
    Triggers:                  +
-   Execution Time: N.N
+   Execution Time: N.N        +
+   Serialization:             +
+     Time: N.N                +
+     Produced: N              +
+     Format: "text"
 (1 row)
 
 select explain_filter('explain (buffers, format text) select * from int8_tbl i8');
@@ -639,3 +643,52 @@ select explain_filter('explain (verbose) select * from int8_tbl i8');
  Query Identifier: N
 (3 rows)
 
+-- Test that SERIALIZE is accepted as a parameter to explain
+-- timings are filtered out by explain_filter
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize) select * from test_serialize');
+                                          explain_filter                                          
+--------------------------------------------------------------------------------------------------
+ Seq Scan on test_serialize  (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ Planning Time: N.N ms
+ Execution Time: N.N ms
+ Serialization:
+   Serialize: time=N.N ms  produced=N bytes  format=text
+(5 rows)
+
+drop table test_serialize;
+-- Test that SERIALIZE BINARY is accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize binary, memory) select * from test_serialize');
+                                          explain_filter                                          
+--------------------------------------------------------------------------------------------------
+ Seq Scan on test_serialize  (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+   Memory: used=N bytes  allocated=N bytes
+ Planning Time: N.N ms
+ Execution Time: N.N ms
+ Serialization:
+   Serialize: time=N.N ms  produced=N bytes  format=binary
+   Memory: used=N bytes  allocated=N bytes
+(7 rows)
+
+drop table test_serialize;
+-- Test that _SERIALIZE invalidparameter_ is not accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize invalidparameter) select * from test_serialize');
+ERROR:  unrecognized value for EXPLAIN option "serialize": "invalidparameter"
+LINE 1: select explain_filter('explain (analyze,serialize invalidpar...
+                         ^
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
+drop table test_serialize;
+-- Test SERIALIZE is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize) select * from test_serialize');
+ERROR:  EXPLAIN option SERIALIZE requires ANALYZE
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
+drop table test_serialize;
+-- Test SERIALIZEBINARY is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize binary) select * from test_serialize');
+ERROR:  EXPLAIN option SERIALIZE requires ANALYZE
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
+drop table test_serialize;
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index 2274dc1b5a..02604b0b14 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -66,7 +66,7 @@ select explain_filter('explain (analyze) select * from int8_tbl i8');
 select explain_filter('explain (analyze, verbose) select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers, format text) select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers, format xml) select * from int8_tbl i8');
-select explain_filter('explain (analyze, buffers, format yaml) select * from int8_tbl i8');
+select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8');
 select explain_filter('explain (buffers, format text) select * from int8_tbl i8');
 select explain_filter('explain (buffers, format json) select * from int8_tbl i8');
 
@@ -162,3 +162,29 @@ select explain_filter('explain (verbose) select * from t1 where pg_temp.mysin(f1
 -- Test compute_query_id
 set compute_query_id = on;
 select explain_filter('explain (verbose) select * from int8_tbl i8');
+
+-- Test that SERIALIZE is accepted as a parameter to explain
+-- timings are filtered out by explain_filter
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize) select * from test_serialize');
+drop table test_serialize;
+
+-- Test that SERIALIZE BINARY is accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize binary, memory) select * from test_serialize');
+drop table test_serialize;
+
+-- Test that _SERIALIZE invalidparameter_ is not accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize invalidparameter) select * from test_serialize');
+drop table test_serialize;
+
+-- Test SERIALIZE is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize) select * from test_serialize');
+drop table test_serialize;
+
+-- Test SERIALIZEBINARY is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize binary) select * from test_serialize');
+drop table test_serialize;
-- 
2.40.1

