/*-------------------------------------------------------------------------
 *
 * profiler.c
 *
 *			  profiler accessories code
 *
 * by Pavel Stehule 2013-2025
 *
 *-------------------------------------------------------------------------
 */
#include "plpgsql_check.h"
#include "plpgsql_check_builtins.h"

#include "access/htup_details.h"
#include "catalog/pg_type.h"
#include "commands/dbcommands.h"
#include "nodes/pg_list.h"
#include "parser/analyze.h"
#include "storage/lwlock.h"
#include "storage/shmem.h"
#include "tcop/tcopprot.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/lsyscache.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/float.h"

#include <math.h>

/*
 * Any instance of plpgsql function will have a own profile.
 * When function will be dropped, then related profile should
 * be removed from shared memory.
 *
 * The local profile is created when function is initialized,
 * and it is stored in plugin_info field. When function is finished,
 * data from local profile is merged to shared profile.
 */
typedef struct profiler_hashkey
{
	Oid			fn_oid;
	Oid			db_oid;
	TransactionId fn_xmin;
	ItemPointerData fn_tid;
	int16		chunk_num;
} profiler_hashkey;

/*
 * We want to collect data about execution on function level. The
 * possible issue can be more different profiles. On this level
 * the version of function is not important, so the hash key is
 * composed only from fn_oid and db_oid
 */
typedef struct fstats_hashkey
{
	Oid			fn_oid;
	Oid			db_oid;
} fstats_hashkey;

typedef struct fstats
{
	fstats_hashkey key;
	slock_t		mutex;
	uint64		exec_count;
	uint64		exec_count_err;
	uint64		total_time;
	double		total_time_xx;
	uint64		min_time;
	uint64		max_time;
} fstats;

/*
 * This is used as cache for types of expressions of USING clause
 * (EXECUTE like statements).
 */
typedef struct query_params
{
	int			nparams;
	Oid			paramtypes[FLEXIBLE_ARRAY_MEMBER];
} query_params;

/*
 * Attention - the commands that can contains nestested commands
 * has attached own time and nested statements time too.
 */
typedef struct profiler_stmt
{
	int			lineno;
	pc_queryid	queryid;
	uint64		us_max;
	uint64		us_total;
	uint64		rows;
	uint64		exec_count;
	uint64		exec_count_err;
	instr_time	start_time;
	instr_time	total;
	bool		has_queryid;
	query_params *qparams;
} profiler_stmt;

typedef struct profiler_stmt_reduced
{
	int			lineno;
	pc_queryid	queryid;
	uint64		us_max;
	uint64		us_total;
	uint64		rows;
	uint64		exec_count;
	uint64		exec_count_err;
	bool		has_queryid;
} profiler_stmt_reduced;

#define		STATEMENTS_PER_CHUNK		30

/*
 * The shared profile will be stored as set of chunks
 */
typedef struct profiler_stmt_chunk
{
	profiler_hashkey key;
	slock_t		mutex;			/* only first chunk require mutex */
	profiler_stmt_reduced stmts[STATEMENTS_PER_CHUNK];
} profiler_stmt_chunk;

typedef struct profiler_shared_state
{
	LWLock	   *lock;
	LWLock	   *fstats_lock;
} profiler_shared_state;

/*
 * This structure is used as plpgsql extension parameter
 */
typedef struct profiler_info
{
	profiler_stmt *stmts;
	int			nstatements;
	instr_time	start_time;
	PLpgSQL_function *func;
} profiler_info;

typedef struct profiler_iterator
{
	profiler_hashkey key;
	plpgsql_check_result_info *ri;
	HTAB	   *chunks;
	profiler_stmt_chunk *current_chunk;
	int			current_statement;

#ifdef USE_ASSERT_CHECKING

	int			current_statement_no;

#endif

} profiler_iterator;

typedef struct profiler_stmt_walker_options
{
	int			stmtid;
	int64		nested_us_time;
	int64		nested_exec_count;
	profiler_iterator *pi;
	coverage_state *cs;
	int		   *stmtid_map;
	plpgsql_check_plugin2_stmt_info *stmts_info;
} profiler_stmt_walker_options;

enum
{
	COVERAGE_STATEMENTS,
	COVERAGE_BRANCHES
};

/*
 * should be enough for project of 300K PLpgSQL rows.
 * It should to take about 24.4MB of shared memory.
 */
int			plpgsql_check_profiler_max_shared_chunks = 15000;

PG_FUNCTION_INFO_V1(plpgsql_check_profiler_ctrl);
PG_FUNCTION_INFO_V1(plpgsql_profiler_reset_all);
PG_FUNCTION_INFO_V1(plpgsql_profiler_reset);
PG_FUNCTION_INFO_V1(plpgsql_coverage_statements);
PG_FUNCTION_INFO_V1(plpgsql_coverage_branches);
PG_FUNCTION_INFO_V1(plpgsql_coverage_statements_name);
PG_FUNCTION_INFO_V1(plpgsql_coverage_branches_name);
PG_FUNCTION_INFO_V1(plpgsql_profiler_install_fake_queryid_hook);
PG_FUNCTION_INFO_V1(plpgsql_profiler_remove_fake_queryid_hook);

static void update_persistent_profile(profiler_info *pinfo, PLpgSQL_function *func, const int *stmtid_map);
static PLpgSQL_expr *profiler_get_expr(PLpgSQL_stmt *stmt, bool *dynamic, List **params);
static pc_queryid profiler_get_queryid(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, bool *has_queryid, query_params **qparams);

#if PG_VERSION_NUM >= 140000

static void profiler_fake_queryid_hook(ParseState *pstate, Query *query, JumbleState *jstate);

#else

static void profiler_fake_queryid_hook(ParseState *pstate, Query *query);

#endif

static void stmts_walker(profiler_info *pinfo, profiler_stmt_walker_mode, List *stmts, PLpgSQL_stmt *parent_stmt,
						 const char *description, profiler_stmt_walker_options *opts);
static void profiler_stmt_walker(profiler_info *pinfo, profiler_stmt_walker_mode mode, PLpgSQL_stmt *stmt,
								 PLpgSQL_stmt *parent_stmt, const char *description, int stmt_block_num, profiler_stmt_walker_options *opts);

static void profiler_func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, void **plugin2_info);
static void profiler_func_end(PLpgSQL_execstate *estate, PLpgSQL_function *func, void **plugin2_info);
static void profiler_func_end_aborted(Oid fn_oid, void **plugin2_info);
static void profiler_stmt_beg(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, void **plugin2_info);
static void profiler_stmt_end(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt, void **plugin2_info);
static void profiler_stmt_end_aborted(Oid fn_oid, int stmtid, void **plugin2_info);


static plpgsql_check_plugin2 profiler_plugin2 =
{
	profiler_func_setup,
	NULL, profiler_func_end, profiler_func_end_aborted,
	profiler_stmt_beg, profiler_stmt_end, profiler_stmt_end_aborted,
	NULL, NULL, NULL, NULL, NULL
};

static HTAB *profiler_HashTable = NULL;
static HTAB *shared_profiler_chunks_HashTable = NULL;
static HTAB *profiler_chunks_HashTable = NULL;
static HTAB *fstats_HashTable = NULL;
static HTAB *shared_fstats_HashTable = NULL;

static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL;

static profiler_shared_state *profiler_ss = NULL;
static MemoryContext profiler_mcxt = NULL;
static MemoryContext profiler_queryid_mcxt = NULL;

bool		plpgsql_check_profiler = false;

/*
 * Use the Youngs-Cramer algorithm to incorporate the new value into the
 * transition values.
 */
static void
eval_stddev_accum(uint64 *_N, uint64 *_Sx, float8 *_Sxx, uint64 newval)
{
	uint64		N = *_N;
	uint64		Sx = *_Sx;
	float8		Sxx = *_Sxx;
	float8		tmp;

	/*
	 * Use the Youngs-Cramer algorithm to incorporate the new value into the
	 * transition values.
	 */
	N += 1;
	Sx += newval;

	if (N > 1)
	{
		tmp = ((float8) newval) * ((float8) N) - ((float8) Sx);

		Sxx += tmp * tmp / (N * (N - 1));

		if (isinf(Sxx))
			Sxx = get_float8_nan();
	}
	else
		Sxx = 0.0;

	*_N = N;
	*_Sx = Sx;
	*_Sxx = Sxx;
}

static profiler_stmt_reduced *
get_stmt_profile_next(profiler_iterator *pi)
{
	if (pi->current_chunk)
	{
		if (pi->current_statement >= STATEMENTS_PER_CHUNK)
		{
			bool		found;

			pi->key.chunk_num += 1;

			pi->current_chunk = (profiler_stmt_chunk *) hash_search(pi->chunks,
																	(void *) &pi->key,
																	HASH_FIND,
																	&found);

			if (!found)
				elog(ERROR, "broken consistency of plpgsql_check profiler chunks");

			pi->current_statement = 0;
		}

#ifdef USE_ASSERT_CHECKING

		pi->current_statement_no += 1;

#endif

		return &pi->current_chunk->stmts[pi->current_statement++];
	}

	return NULL;
}

/*
 * Calculate required size of shared memory for chunks
 *
 */
Size
plpgsql_check_shmem_size(void)
{
	Size		num_bytes = 0;

	num_bytes = MAXALIGN(sizeof(profiler_shared_state));
	num_bytes = add_size(num_bytes,
						 hash_estimate_size(plpgsql_check_profiler_max_shared_chunks,
											sizeof(profiler_stmt_chunk)));

	return num_bytes;
}

/*
 * Request additional shared memory resources.
 *
 * If you change code here, don't forget to also report the modifications in
 * _PG_init() for pg14 and below.
 */
#if PG_VERSION_NUM >= 150000

void
plpgsql_check_profiler_shmem_request(void)
{
	if (plpgsql_check_prev_shmem_request_hook)
		plpgsql_check_prev_shmem_request_hook();

	RequestAddinShmemSpace(plpgsql_check_shmem_size());

	RequestNamedLWLockTranche("plpgsql_check profiler", 1);
	RequestNamedLWLockTranche("plpgsql_check fstats", 1);
}

#endif

/*
 * Initialize shared memory used like permanent profile storage.
 * No other parts use shared memory, so this code is completly here.
 *
 */
void
plpgsql_check_profiler_shmem_startup(void)
{
	bool		found;
	HASHCTL		info;

	shared_profiler_chunks_HashTable = NULL;
	shared_fstats_HashTable = NULL;

	if (plpgsql_check_prev_shmem_startup_hook)
		plpgsql_check_prev_shmem_startup_hook();

	/*
	 * Create or attach to the shared memory state, including hash table
	 */
	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);

	profiler_ss = ShmemInitStruct("plpgsql_check profiler state",
								  sizeof(profiler_shared_state),
								  &found);

	if (!found)
	{
		profiler_ss->lock = &(GetNamedLWLockTranche("plpgsql_check profiler"))->lock;
		profiler_ss->fstats_lock = &(GetNamedLWLockTranche("plpgsql_check fstats"))->lock;
	}

	memset(&info, 0, sizeof(info));
	info.keysize = sizeof(profiler_hashkey);
	info.entrysize = sizeof(profiler_stmt_chunk);

	shared_profiler_chunks_HashTable = ShmemInitHash("plpgsql_check profiler chunks",
													 plpgsql_check_profiler_max_shared_chunks,
													 plpgsql_check_profiler_max_shared_chunks,
													 &info,
													 HASH_ELEM | HASH_BLOBS);

	memset(&info, 0, sizeof(info));
	info.keysize = sizeof(fstats_hashkey);
	info.entrysize = sizeof(fstats);

	shared_fstats_HashTable = ShmemInitHash("plpgsql_check fstats",
											500,
											1000,
											&info,
											HASH_ELEM | HASH_BLOBS);

	LWLockRelease(AddinShmemInitLock);
}

/*
 * Profiler implementation
 */

static void
profiler_init_hashkey(profiler_hashkey *hk, PLpgSQL_function *func)
{
	memset(hk, 0, sizeof(profiler_hashkey));

	hk->db_oid = MyDatabaseId;
	hk->fn_oid = func->fn_oid;

#if PG_VERSION_NUM >= 180000

	hk->fn_xmin = func->cfunc.fn_xmin;
	hk->fn_tid = func->cfunc.fn_tid;

#else

	hk->fn_xmin = func->fn_xmin;
	hk->fn_tid = func->fn_tid;

#endif

	hk->chunk_num = 1;
}

/*
 * Hash table for local function profiles. When shared memory is not available
 * because plpgsql_check was not loaded by shared_proload_libraries, then function
 * profiles is stored in local profile chunks. A format is same for shared profiles.
 */
static void
profiler_chunks_HashTableInit(void)
{
	HASHCTL		ctl;

	Assert(profiler_chunks_HashTable == NULL);

	memset(&ctl, 0, sizeof(ctl));
	ctl.keysize = sizeof(profiler_hashkey);
	ctl.entrysize = sizeof(profiler_stmt_chunk);
	ctl.hcxt = profiler_mcxt;
	profiler_chunks_HashTable = hash_create("plpgsql_check function profiler local chunks",
											FUNCS_PER_USER,
											&ctl,
											HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}

static void
fstats_init_hashkey(fstats_hashkey *fhk, Oid fn_oid)
{
	memset(fhk, 0, sizeof(fstats_hashkey));

	fhk->db_oid = MyDatabaseId;
	fhk->fn_oid = fn_oid;
}

static void
fstats_HashTableInit(void)
{
	HASHCTL		ctl;

	Assert(fstats_HashTable == NULL);

	memset(&ctl, 0, sizeof(ctl));
	ctl.keysize = sizeof(fstats_hashkey);
	ctl.entrysize = sizeof(fstats);
	ctl.hcxt = profiler_mcxt;
	fstats_HashTable = hash_create("plpgsql_check function execution statistics",
								   FUNCS_PER_USER,
								   &ctl,
								   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}

void
plpgsql_check_profiler_init_hash_tables(void)
{
	if (profiler_mcxt)
	{
		MemoryContextReset(profiler_mcxt);

		profiler_HashTable = NULL;
		profiler_chunks_HashTable = NULL;
		fstats_HashTable = NULL;
	}
	else
	{
		profiler_mcxt = AllocSetContextCreate(TopMemoryContext,
											  "plpgsql_check - profiler context",
											  ALLOCSET_DEFAULT_MINSIZE,
											  ALLOCSET_DEFAULT_INITSIZE,
											  ALLOCSET_DEFAULT_MAXSIZE);
	}

	profiler_chunks_HashTableInit();
	fstats_HashTableInit();
}

/*
 * Increase a branch couter - used for branch coverage
 */
static void
increment_branch_counter(coverage_state *cs, int64 executed)
{
	Assert(cs);

	cs->branches += 1;
	cs->executed_branches += executed > 0 ? 1 : 0;
}

#define IS_PLPGSQL_STMT(stmt, typ)		(stmt->cmd_type == typ)

static bool
is_cycle(PLpgSQL_stmt *stmt)
{
	switch (stmt->cmd_type)
	{
		case PLPGSQL_STMT_LOOP:
		case PLPGSQL_STMT_FORI:
		case PLPGSQL_STMT_FORS:
		case PLPGSQL_STMT_FORC:
		case PLPGSQL_STMT_DYNFORS:
		case PLPGSQL_STMT_FOREACH_A:
		case PLPGSQL_STMT_WHILE:
			return true;
		default:
			return false;
	}
}

/*
 * Returns statements assigned to cycle's body
 */
static List *
get_cycle_body(PLpgSQL_stmt *stmt)
{
	List	   *stmts;

	switch (stmt->cmd_type)
	{
		case PLPGSQL_STMT_WHILE:
			stmts = ((PLpgSQL_stmt_while *) stmt)->body;
			break;
		case PLPGSQL_STMT_LOOP:
			stmts = ((PLpgSQL_stmt_loop *) stmt)->body;
			break;
		case PLPGSQL_STMT_FORI:
			stmts = ((PLpgSQL_stmt_fori *) stmt)->body;
			break;
		case PLPGSQL_STMT_FORS:
			stmts = ((PLpgSQL_stmt_fors *) stmt)->body;
			break;
		case PLPGSQL_STMT_FORC:
			stmts = ((PLpgSQL_stmt_forc *) stmt)->body;
			break;
		case PLPGSQL_STMT_DYNFORS:
			stmts = ((PLpgSQL_stmt_dynfors *) stmt)->body;
			break;
		case PLPGSQL_STMT_FOREACH_A:
			stmts = ((PLpgSQL_stmt_foreach_a *) stmt)->body;
			break;
		default:
			stmts = NIL;
			break;
	}

	return stmts;
}

/*
 * profiler_stmt_walker - iterator over plpgsql statements.
 *
 * This function is designed for three different purposes:
 *
 *   a) iterate over all commends and finalize total time
 *      as measured total time substract child total time.
 *   b) iterate over all commands and prepare result for
 *      plpgsql_profiler_function_statements_tb function.
 *   c) iterate over all commands to collect code coverage
 *      metrics
 */
static void
profiler_stmt_walker(profiler_info *pinfo,
					 profiler_stmt_walker_mode mode,
					 PLpgSQL_stmt *stmt,
					 PLpgSQL_stmt *parent_stmt,
					 const char *description,
					 int stmt_block_num,
					 profiler_stmt_walker_options *opts)
{
	profiler_stmt *pstmt = NULL;
	bool		count_exec_time_mode = mode == PLPGSQL_CHECK_STMT_WALKER_COUNT_EXEC_TIME;
	bool		prepare_result_mode = mode == PLPGSQL_CHECK_STMT_WALKER_PREPARE_RESULT;
	bool		collect_coverage_mode = mode == PLPGSQL_CHECK_STMT_WALKER_COLLECT_COVERAGE;

	int64		nested_us_time = 0;
	int64		exec_count = 0;

	int			stmtid = -1;

	char		strbuf[100];
	int			n = 0;
	List	   *stmts;
	ListCell   *lc;

	stmtid = stmt->stmtid - 1;

	if (count_exec_time_mode)
	{
		/*
		 * Get statement info from function execution context by statement id.
		 */
		pstmt = &pinfo->stmts[stmtid];
		pstmt->lineno = stmt->lineno;
	}
	else
	{
		profiler_stmt_reduced *ppstmt = NULL;

		Assert(opts->pi);

		/*
		 * When iterator is used, then id of iterator's current statement have
		 * to be same like stmtid of stmt. When function was not executed in
		 * active profile mode, then we have not any stored chunk, and
		 * iterator returns 0 stmtid.
		 */
		Assert(!opts->pi->current_chunk ||
			   (opts->stmtid_map[opts->pi->current_statement_no] - 1) == stmtid);

		/*
		 * Get persistent statement info stored in shared memory or in session
		 * memory by iterator.
		 */
		ppstmt = get_stmt_profile_next(opts->pi);

		if (prepare_result_mode && opts->pi->ri)
		{
			plpgsql_check_plugin2_stmt_info *sinfo;
			int			parent_natural_stmtid = -1;

			parent_natural_stmtid = parent_stmt ? opts->stmts_info[parent_stmt->stmtid - 1].natural_id : -1;
			sinfo = &opts->stmts_info[stmt->stmtid - 1];

			plpgsql_check_put_profile_statement(opts->pi->ri,
												ppstmt ? ppstmt->queryid : NOQUERYID,
												sinfo->natural_id,
												parent_natural_stmtid,
												description,
												stmt_block_num,
												stmt->lineno,
												ppstmt ? ppstmt->exec_count : 0,
												ppstmt ? ppstmt->exec_count_err : 0,
												ppstmt ? ppstmt->us_total : 0.0,
												ppstmt ? ppstmt->us_max : 0.0,
												ppstmt ? ppstmt->rows : 0,
												(char *) sinfo->typname);
		}
		else if (collect_coverage_mode)
		{
			/* save statement exec count */
			exec_count = ppstmt ? ppstmt->exec_count : 0;

			/* ignore invisible BLOCK */
			if (stmt->lineno != -1)
			{
				opts->cs->statements += 1;
				opts->cs->executed_statements += exec_count > 0 ? 1 : 0;
			}
		}
	}

	if (is_cycle(stmt))
	{
		stmts = get_cycle_body(stmt);

		stmts_walker(pinfo, mode,
					 stmts, stmt, "loop body",
					 opts);

		if (collect_coverage_mode)
			increment_branch_counter(opts->cs,
									 opts->nested_exec_count);
	}
	else if (IS_PLPGSQL_STMT(stmt, PLPGSQL_STMT_IF))
	{
		PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt;
		int64		all_nested_branches_exec_count = 0;

		/*
		 * Note: when if statement has not else path, then we have to
		 * calculate deduce number of execution of implicit else path
		 * manually. We know number of execution of IF statement and we can
		 * subtract an number of execution of nested paths.
		 */

		stmts_walker(pinfo, mode,
					 stmt_if->then_body, stmt, "then body",
					 opts);

		if (count_exec_time_mode)
		{
			nested_us_time = opts->nested_us_time;
		}
		else if (collect_coverage_mode)
		{
			increment_branch_counter(opts->cs,
									 opts->nested_exec_count);

			all_nested_branches_exec_count += opts->nested_exec_count;
		}

		foreach(lc, stmt_if->elsif_list)
		{
			stmts = ((PLpgSQL_if_elsif *) lfirst(lc))->stmts;

			sprintf(strbuf, "elsif %d", ++n);

			stmts_walker(pinfo, mode,
						 stmts, stmt, strbuf,
						 opts);

			if (count_exec_time_mode)
				nested_us_time += opts->nested_us_time;

			else if (collect_coverage_mode)
			{
				increment_branch_counter(opts->cs,
										 opts->nested_exec_count);

				all_nested_branches_exec_count += opts->nested_exec_count;
			}
		}

		if (stmt_if->else_body)
		{
			stmts_walker(pinfo, mode,
						 stmt_if->else_body, stmt, "else body",
						 opts);

			if (count_exec_time_mode)
				nested_us_time += opts->nested_us_time;

			else if (collect_coverage_mode)
				increment_branch_counter(opts->cs,
										 opts->nested_exec_count);
		}
		else
		{
			/* calculate exec_count for implicit else path */
			if (collect_coverage_mode)
			{
				int64		else_exec_count = exec_count - all_nested_branches_exec_count;

				increment_branch_counter(opts->cs,
										 else_exec_count);
			}
		}
	}
	else if (IS_PLPGSQL_STMT(stmt, PLPGSQL_STMT_CASE))
	{
		PLpgSQL_stmt_case *stmt_case = (PLpgSQL_stmt_case *) stmt;

		foreach(lc, stmt_case->case_when_list)
		{
			stmts = ((PLpgSQL_case_when *) lfirst(lc))->stmts;

			sprintf(strbuf, "case when %d", ++n);

			stmts_walker(pinfo, mode,
						 stmts, stmt, strbuf,
						 opts);

			if (count_exec_time_mode)
				nested_us_time = opts->nested_us_time;

			else if (collect_coverage_mode)
				increment_branch_counter(opts->cs,
										 opts->nested_exec_count);
		}

		stmts_walker(pinfo, mode,
					 stmt_case->else_stmts, stmt, "case else",
					 opts);

		if (count_exec_time_mode)
			nested_us_time = opts->nested_us_time;

		else if (collect_coverage_mode)
			increment_branch_counter(opts->cs,
									 opts->nested_exec_count);
	}
	else if (IS_PLPGSQL_STMT(stmt, PLPGSQL_STMT_BLOCK))
	{
		PLpgSQL_stmt_block *stmt_block = (PLpgSQL_stmt_block *) stmt;

		stmts_walker(pinfo, mode,
					 stmt_block->body, stmt, "body",
					 opts);

		if (count_exec_time_mode)
			nested_us_time = opts->nested_us_time;

		if (stmt_block->exceptions)
		{
			if (collect_coverage_mode)
				increment_branch_counter(opts->cs,
										 opts->nested_exec_count);

			foreach(lc, stmt_block->exceptions->exc_list)
			{
				stmts = ((PLpgSQL_exception *) lfirst(lc))->action;

				sprintf(strbuf, "exception %d", ++n);

				stmts_walker(pinfo, mode,
							 stmts, stmt, strbuf,
							 opts);

				if (count_exec_time_mode)
					nested_us_time += opts->nested_us_time;
				else if (collect_coverage_mode)
					increment_branch_counter(opts->cs,
											 opts->nested_exec_count);
			}
		}
	}

	if (count_exec_time_mode)
	{
		Assert(pstmt);

		opts->nested_us_time = pstmt->us_total;
		pstmt->us_total -= nested_us_time;

		/*
		 * When max time is unknown, but statement was executed only only
		 * once, we can se max time.
		 */
		if (pstmt->exec_count == 1 && pstmt->us_max == 1)
			pstmt->us_max = pstmt->us_total;
	}
	else if (collect_coverage_mode)
	{
		opts->nested_exec_count = exec_count;
	}
}

/*
 * Enable, disable, show state profiler
 */
Datum
plpgsql_check_profiler_ctrl(PG_FUNCTION_ARGS)
{
	char	   *optstr;
	bool		result;

#define OPTNAME		"plpgsql_check.profiler"

	if (!PG_ARGISNULL(0))
	{
		bool		optval = PG_GETARG_BOOL(0);

		(void) set_config_option(OPTNAME, optval ? "on" : "off",
								 (superuser() ? PGC_SUSET : PGC_USERSET),
								 PGC_S_SESSION, GUC_ACTION_SET,
								 true, 0, false);
	}

	optstr = GetConfigOptionByName(OPTNAME, NULL, false);

	if (strcmp(optstr, "on") == 0)
	{
		elog(NOTICE, "profiler is active");
		result = true;
	}
	else
	{
		elog(NOTICE, "profiler is not active");
		result = false;
	};

	PG_RETURN_BOOL(result);
}

/*
 * clean all chunks used by profiler
 */
Datum
plpgsql_profiler_reset_all(PG_FUNCTION_ARGS)
{
	/* be compiler quite */
	(void) fcinfo;

	if (shared_profiler_chunks_HashTable)
	{
		HASH_SEQ_STATUS hash_seq;
		profiler_stmt_chunk *chunk;
		fstats	   *fstats_entry;

		LWLockAcquire(profiler_ss->lock, LW_EXCLUSIVE);

		hash_seq_init(&hash_seq, shared_profiler_chunks_HashTable);

		while ((chunk = hash_seq_search(&hash_seq)) != NULL)
		{
			hash_search(shared_profiler_chunks_HashTable,
						&(chunk->key),
						HASH_REMOVE,
						NULL);
		}

		LWLockRelease(profiler_ss->lock);

		Assert(shared_fstats_HashTable);

		LWLockAcquire(profiler_ss->fstats_lock, LW_EXCLUSIVE);

		hash_seq_init(&hash_seq, shared_fstats_HashTable);

		while ((fstats_entry = hash_seq_search(&hash_seq)) != NULL)
		{
			hash_search(shared_fstats_HashTable,
						&(fstats_entry->key),
						HASH_REMOVE,
						NULL);
		}

		LWLockRelease(profiler_ss->fstats_lock);
	}

	plpgsql_check_profiler_init_hash_tables();

	PG_RETURN_VOID();
}

/*
 * Clean chunks related to some function
 */
Datum
plpgsql_profiler_reset(PG_FUNCTION_ARGS)
{
	Oid			funcoid = PG_GETARG_OID(0);
	profiler_hashkey hk;
	fstats_hashkey fhk;
	HTAB	   *chunks;
	HeapTuple	procTuple;
	bool		found;
	bool		shared_chunks;

	procTuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid));
	if (!HeapTupleIsValid(procTuple))
		elog(ERROR, "cache lookup failed for function %u", funcoid);

	/* ensure correct complete content of hash key */
	memset(&hk, 0, sizeof(profiler_hashkey));
	hk.fn_oid = funcoid;
	hk.db_oid = MyDatabaseId;
	hk.fn_xmin = HeapTupleHeaderGetRawXmin(procTuple->t_data);
	hk.fn_tid = procTuple->t_self;
	hk.chunk_num = 1;

	ReleaseSysCache(procTuple);

	if (shared_profiler_chunks_HashTable)
	{
		LWLockAcquire(profiler_ss->lock, LW_EXCLUSIVE);
		chunks = shared_profiler_chunks_HashTable;
		shared_chunks = true;
	}
	else
	{
		chunks = profiler_chunks_HashTable;
		shared_chunks = false;
	}

	for (;;)
	{
		hash_search(chunks, (void *) &hk, HASH_REMOVE, &found);
		if (!found)
			break;
		hk.chunk_num += 1;
	}

	if (shared_chunks)
		LWLockRelease(profiler_ss->lock);

	fstats_init_hashkey(&fhk, funcoid);

	if (shared_fstats_HashTable)
	{
		LWLockAcquire(profiler_ss->fstats_lock, LW_EXCLUSIVE);

		hash_search(shared_fstats_HashTable, (void *) &fhk, HASH_REMOVE, NULL);

		LWLockRelease(profiler_ss->fstats_lock);
	}
	else
		hash_search(fstats_HashTable, (void *) &fhk, HASH_REMOVE, NULL);

	PG_RETURN_VOID();
}

/*
 * Prepare environment for reading profile and calculation of coverage metric
 */
static double
coverage_internal(Oid fnoid, int coverage_type)
{
	plpgsql_check_info cinfo;
	coverage_state cs;

	memset(&cs, 0, sizeof(cs));

	plpgsql_check_info_init(&cinfo, fnoid);
	cinfo.show_profile = true;

	cinfo.proctuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(cinfo.fn_oid));
	if (!HeapTupleIsValid(cinfo.proctuple))
		elog(ERROR, "cache lookup failed for function %u", cinfo.fn_oid);

	plpgsql_check_get_function_info(&cinfo);

	plpgsql_check_precheck_conditions(&cinfo);

	plpgsql_check_iterate_over_profile(&cinfo,
									   PLPGSQL_CHECK_STMT_WALKER_COLLECT_COVERAGE,
									   NULL, &cs);

	ReleaseSysCache(cinfo.proctuple);

	if (coverage_type == COVERAGE_STATEMENTS)
	{
		if (cs.statements > 0)
			return (double) cs.executed_statements / (double) cs.statements;
		else
			return (double) 1.0;
	}
	else
	{
		if (cs.branches > 0)
			return (double) cs.executed_branches / (double) cs.branches;
		else
			return (double) 1.0;
	}
}

Datum
plpgsql_coverage_statements_name(PG_FUNCTION_ARGS)
{
	Oid			fnoid;
	char	   *name_or_signature;

	if (PG_ARGISNULL(0))
		elog(ERROR, "the first argument should not be null");

	name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0));
	fnoid = plpgsql_check_parse_name_or_signature(name_or_signature);

	PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_STATEMENTS));
}

Datum
plpgsql_coverage_branches_name(PG_FUNCTION_ARGS)
{
	Oid			fnoid;
	char	   *name_or_signature;

	if (PG_ARGISNULL(0))
		elog(ERROR, "the first argument should not be null");

	name_or_signature = text_to_cstring(PG_GETARG_TEXT_PP(0));
	fnoid = plpgsql_check_parse_name_or_signature(name_or_signature);

	PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_BRANCHES));
}

Datum
plpgsql_coverage_statements(PG_FUNCTION_ARGS)
{
	Oid			fnoid;

	if (PG_ARGISNULL(0))
		elog(ERROR, "the first argument should not be null");

	fnoid = PG_GETARG_OID(0);

	PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_STATEMENTS));
}

Datum
plpgsql_coverage_branches(PG_FUNCTION_ARGS)
{
	Oid			fnoid;

	if (PG_ARGISNULL(0))
		elog(ERROR, "the first argument should not be null");

	fnoid = PG_GETARG_OID(0);

	PG_RETURN_FLOAT8(coverage_internal(fnoid, COVERAGE_BRANCHES));
}

Datum
plpgsql_profiler_install_fake_queryid_hook(PG_FUNCTION_ARGS)
{
	(void) fcinfo;

	if (post_parse_analyze_hook == profiler_fake_queryid_hook)
		PG_RETURN_VOID();

	if (post_parse_analyze_hook == NULL)
		prev_post_parse_analyze_hook = post_parse_analyze_hook;

	post_parse_analyze_hook = profiler_fake_queryid_hook;

	PG_RETURN_VOID();
}

Datum
plpgsql_profiler_remove_fake_queryid_hook(PG_FUNCTION_ARGS)
{
	(void) fcinfo;

	if (post_parse_analyze_hook == profiler_fake_queryid_hook)
	{
		post_parse_analyze_hook = prev_post_parse_analyze_hook;
		prev_post_parse_analyze_hook = NULL;
	}

	PG_RETURN_VOID();
}

static void
update_persistent_fstats(PLpgSQL_function *func,
						 uint64 elapsed)
{
	HTAB	   *fstats_ht;
	bool		htab_is_shared;
	fstats_hashkey fhk;
	fstats	   *fstats_item;
	bool		found;
	bool		use_spinlock = false;

	fstats_init_hashkey(&fhk, func->fn_oid);

	/* try to find first chunk in shared (or local) memory */
	if (shared_fstats_HashTable)
	{
		LWLockAcquire(profiler_ss->fstats_lock, LW_SHARED);
		fstats_ht = shared_fstats_HashTable;
		htab_is_shared = true;
	}
	else
	{
		fstats_ht = fstats_HashTable;
		htab_is_shared = false;
	}

	fstats_item = (fstats *) hash_search(fstats_ht,
										 (void *) &fhk,
										 HASH_FIND,
										 &found);

	if (!found)
	{
		if (htab_is_shared)
		{
			LWLockRelease(profiler_ss->fstats_lock);
			LWLockAcquire(profiler_ss->fstats_lock, LW_EXCLUSIVE);
		}

		fstats_item = (fstats *) hash_search(fstats_ht,
											 (void *) &fhk,
											 HASH_ENTER,
											 &found);
	}

	if (!fstats_item)
		elog(ERROR,
			 "cannot to insert new entry to profiler's function statistics");

	if (htab_is_shared)
	{
		if (found)
		{
			SpinLockAcquire(&fstats_item->mutex);
			use_spinlock = true;
		}
		else
			SpinLockInit(&fstats_item->mutex);
	}

	if (!found)
	{
		fstats_item->exec_count = 0;
		fstats_item->exec_count_err = 0;
		fstats_item->total_time = 0;
		fstats_item->total_time_xx = 0.0;
		fstats_item->min_time = elapsed;
		fstats_item->max_time = elapsed;
	}
	else
	{
		fstats_item->min_time = fstats_item->min_time < elapsed ? fstats_item->min_time : elapsed;
		fstats_item->max_time = fstats_item->max_time > elapsed ? fstats_item->max_time : elapsed;
	}

	eval_stddev_accum(&fstats_item->exec_count,
					  &fstats_item->total_time,
					  &fstats_item->total_time_xx,
					  elapsed);

	if (use_spinlock)
		SpinLockRelease(&fstats_item->mutex);

	if (htab_is_shared)
		LWLockRelease(profiler_ss->fstats_lock);
}

static void
update_persistent_profile(profiler_info *pinfo,
						  PLpgSQL_function *func,
						  const int *stmtid_map)
{
	profiler_hashkey hk;
	profiler_stmt_chunk *chunk = NULL;
	bool		found;
	HTAB	   *chunks;
	bool		shared_chunks;

	volatile profiler_stmt_chunk *chunk_with_mutex = NULL;

	if (shared_profiler_chunks_HashTable)
	{
		chunks = shared_profiler_chunks_HashTable;
		LWLockAcquire(profiler_ss->lock, LW_SHARED);
		shared_chunks = true;
	}
	else
	{
		chunks = profiler_chunks_HashTable;
		shared_chunks = false;
	}

	profiler_init_hashkey(&hk, func);

	/* don't need too strong lock for reading shared memory */
	chunk = (profiler_stmt_chunk *) hash_search(chunks,
												(void *) &hk,
												HASH_FIND,
												&found);

	/* We need exclusive lock, when we want to add new chunk */
	if (!found && shared_chunks)
	{
		LWLockRelease(profiler_ss->lock);
		LWLockAcquire(profiler_ss->lock, LW_EXCLUSIVE);

		/* repeat searching under exclusive lock */
		chunk = (profiler_stmt_chunk *) hash_search(chunks,
													(void *) &hk,
													HASH_FIND,
													&found);
	}

	if (!found)
	{
		int			stmt_counter = 0;
		int			i;

		/* aftre increment first chunk will be created with chunk number 1 */
		hk.chunk_num = 0;

		/* we should to enter empty chunks first */

		for (i = 0; i < func->nstatements; i++)
		{
			volatile profiler_stmt_reduced *prstmt;
			profiler_stmt *pstmt;

			/*
			 * We need to store statement statistics to chunks in natural
			 * order next statistics should be related to statement on same or
			 * higher line. Unfortunately buildin stmtid has inverse order
			 * based on bison parser processing statement should to be on same
			 * or higher line)
			 *
			 */
			int			n = stmtid_map[i] - 1;

			/* Skip gaps in reorder map */
			if (n == -1)
				continue;

			pstmt = &pinfo->stmts[n];

			if (hk.chunk_num == 0 || stmt_counter >= STATEMENTS_PER_CHUNK)
			{
				hk.chunk_num += 1;

				chunk = (profiler_stmt_chunk *) hash_search(chunks,
															(void *) &hk,
															HASH_ENTER,
															&found);

				if (found)
					elog(ERROR, "broken consistency of plpgsql_check profiler chunks");

				if (hk.chunk_num == 1 && shared_chunks)
					SpinLockInit(&chunk->mutex);

				stmt_counter = 0;
			}

			prstmt = &chunk->stmts[stmt_counter++];

			prstmt->lineno = pstmt->lineno;
			prstmt->queryid = pstmt->queryid;
			prstmt->has_queryid = pstmt->has_queryid;
			prstmt->us_max = pstmt->us_max;
			prstmt->us_total = pstmt->us_total;
			prstmt->rows = pstmt->rows;
			prstmt->exec_count = pstmt->exec_count;
			prstmt->exec_count_err = pstmt->exec_count_err;
		}

		/* clean unused stmts in chunk */
		while (stmt_counter < STATEMENTS_PER_CHUNK)
			chunk->stmts[stmt_counter++].lineno = -1;

		if (shared_chunks)
			LWLockRelease(profiler_ss->lock);

		return;
	}

	/*
	 * Now we know, so there is already profile, and we have all necessary
	 * locks. Teoreticaly, we can reuse existing chunk, but inside PG_TRY
	 * block is better to take this value again to fix warning - "might be
	 * clobbered by 'longjmp"
	 */
	PG_TRY();
	{
		profiler_stmt_chunk *_chunk = NULL;
		HTAB	   *_chunks;
		int			stmt_counter = 0;
		int			i = 0;

		_chunks = shared_chunks ? shared_profiler_chunks_HashTable : profiler_chunks_HashTable;
		profiler_init_hashkey(&hk, func);

		/* search chunk again */
		_chunk = (profiler_stmt_chunk *) hash_search(_chunks,
													 (void *) &hk,
													 HASH_FIND,
													 &found);

		if (shared_chunks)
		{
			chunk_with_mutex = _chunk;
			SpinLockAcquire(&chunk_with_mutex->mutex);
		}
		else
			chunk_with_mutex = NULL;

		hk.chunk_num = 1;
		stmt_counter = 0;

		/* there is a profiler chunk already */
		for (i = 0; i < func->nstatements; i++)
		{
			volatile profiler_stmt_reduced *prstmt;
			profiler_stmt *pstmt;

			/*
			 * We need to store statement statistics to chunks in natural
			 * order (next statistics should be related to statement on same
			 * or higher line). Unfortunately buildin stmtid has inverse order
			 * based on bison parser processing statement.
			 */
			int			n = stmtid_map[i] - 1;

			/* Skip gaps in reorder map */
			if (n == -1)
				continue;

			pstmt = &pinfo->stmts[n];

			if (stmt_counter >= STATEMENTS_PER_CHUNK)
			{
				hk.chunk_num += 1;

				_chunk = (profiler_stmt_chunk *) hash_search(_chunks,
															 (void *) &hk,
															 HASH_FIND,
															 &found);

				if (!found)
					elog(ERROR, "broken consistency of plpgsql_check profiler chunks");

				stmt_counter = 0;
			}

			prstmt = &_chunk->stmts[stmt_counter++];

			if (prstmt->lineno != pstmt->lineno)
				elog(ERROR, "broken consistency of plpgsql_check profiler chunks %d %d", prstmt->lineno, pstmt->lineno);

			if (prstmt->us_max < pstmt->us_max)
				prstmt->us_max = pstmt->us_max;

			prstmt->us_total += pstmt->us_total;
			prstmt->rows += pstmt->rows;
			prstmt->exec_count += pstmt->exec_count;
			prstmt->exec_count_err += pstmt->exec_count_err;
		}
	}
	PG_CATCH();
	{
		if (chunk_with_mutex)
			SpinLockRelease(&chunk_with_mutex->mutex);
		PG_RE_THROW();
	}
	PG_END_TRY();

	if (chunk_with_mutex)
		SpinLockRelease(&chunk_with_mutex->mutex);

	if (shared_chunks)
		LWLockRelease(profiler_ss->lock);
}

/*
 * Iterate over list of statements
 */
static void
stmts_walker(profiler_info *pinfo,
			 profiler_stmt_walker_mode mode,
			 List *stmts,
			 PLpgSQL_stmt *parent_stmt,
			 const char *description,
			 profiler_stmt_walker_options *opts)
{
	bool		count_exec_time = mode == PLPGSQL_CHECK_STMT_WALKER_COUNT_EXEC_TIME;
	bool		collect_coverage = mode == PLPGSQL_CHECK_STMT_WALKER_COLLECT_COVERAGE;

	int64		nested_us_time = 0;
	int64		nested_exec_count = 0;

	int			stmt_block_num = 1;

	ListCell   *lc;

	foreach(lc, stmts)
	{
		PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) lfirst(lc);

		profiler_stmt_walker(pinfo, mode,
							 stmt, parent_stmt, description,
							 stmt_block_num,
							 opts);

		/* add stmt execution time to total execution time */
		if (count_exec_time)
			nested_us_time += opts->nested_us_time;

		/*
		 * For calculation of coverage we need a numbers of nested statements
		 * execution. Usually or statements in list has same number of
		 * execution. But it should not be true for some reasons (after RETURN
		 * or some exception). I am not sure if following simplification is
		 * accurate, but maybe. I use number of execution of first statement
		 * in block like number of execution all statements in list.
		 */
		if (collect_coverage && stmt_block_num == 1)
			nested_exec_count = opts->nested_exec_count;

		stmt_block_num += 1;
	}

	if (count_exec_time)
		opts->nested_us_time = nested_us_time;

	if (collect_coverage)
		opts->nested_exec_count = nested_exec_count;
}

/*
 * Given a PLpgSQL_stmt, return the underlying PLpgSQL_expr that may contain a
 * queryid.
 */
static PLpgSQL_expr *
profiler_get_expr(PLpgSQL_stmt *stmt, bool *dynamic, List **params)
{
	PLpgSQL_expr *expr = NULL;

	*params = NIL;
	*dynamic = false;

	switch (stmt->cmd_type)
	{
		case PLPGSQL_STMT_ASSIGN:
			expr = ((PLpgSQL_stmt_assign *) stmt)->expr;
			break;
		case PLPGSQL_STMT_PERFORM:
			expr = ((PLpgSQL_stmt_perform *) stmt)->expr;
			break;
		case PLPGSQL_STMT_CALL:
			expr = ((PLpgSQL_stmt_call *) stmt)->expr;
			break;
		case PLPGSQL_STMT_IF:
			expr = ((PLpgSQL_stmt_if *) stmt)->cond;
			break;
		case PLPGSQL_STMT_CASE:
			expr = ((PLpgSQL_stmt_case *) stmt)->t_expr;
			break;
		case PLPGSQL_STMT_WHILE:
			expr = ((PLpgSQL_stmt_while *) stmt)->cond;
			break;
		case PLPGSQL_STMT_FORC:
			expr = ((PLpgSQL_stmt_forc *) stmt)->argquery;
			break;
		case PLPGSQL_STMT_FORS:
			expr = ((PLpgSQL_stmt_fors *) stmt)->query;
			break;
		case PLPGSQL_STMT_DYNFORS:
			expr = ((PLpgSQL_stmt_dynfors *) stmt)->query;
			*params = ((PLpgSQL_stmt_dynfors *) stmt)->params;
			*dynamic = true;
			break;
		case PLPGSQL_STMT_FOREACH_A:
			expr = ((PLpgSQL_stmt_foreach_a *) stmt)->expr;
			break;
		case PLPGSQL_STMT_FETCH:
			expr = ((PLpgSQL_stmt_fetch *) stmt)->expr;
			break;
		case PLPGSQL_STMT_EXIT:
			expr = ((PLpgSQL_stmt_exit *) stmt)->cond;
			break;
		case PLPGSQL_STMT_RETURN:
			expr = ((PLpgSQL_stmt_return *) stmt)->expr;
			break;
		case PLPGSQL_STMT_RETURN_NEXT:
			expr = ((PLpgSQL_stmt_return_next *) stmt)->expr;
			break;
		case PLPGSQL_STMT_RETURN_QUERY:
			{
				PLpgSQL_stmt_return_query *q;

				q = (PLpgSQL_stmt_return_query *) stmt;
				if (q->query)
					expr = q->query;
				else
				{
					expr = q->dynquery;
					*params = q->params;
					*dynamic = true;
				}
			}
			break;
		case PLPGSQL_STMT_ASSERT:
			expr = ((PLpgSQL_stmt_assert *) stmt)->cond;
			break;
		case PLPGSQL_STMT_EXECSQL:
			expr = ((PLpgSQL_stmt_execsql *) stmt)->sqlstmt;
			break;
		case PLPGSQL_STMT_DYNEXECUTE:
			expr = ((PLpgSQL_stmt_dynexecute *) stmt)->query;
			*params = ((PLpgSQL_stmt_dynexecute *) stmt)->params;
			*dynamic = true;
			break;
		case PLPGSQL_STMT_OPEN:
			{
				PLpgSQL_stmt_open *o;

				o = (PLpgSQL_stmt_open *) stmt;
				if (o->query)
					expr = o->query;
				else if (o->dynquery)
				{
					expr = o->dynquery;
					*params = o->params;
					*dynamic = true;
				}
				else
					expr = o->argquery;
			}
		case PLPGSQL_STMT_BLOCK:
		case PLPGSQL_STMT_COMMIT:
		case PLPGSQL_STMT_ROLLBACK:
		case PLPGSQL_STMT_GETDIAG:
		case PLPGSQL_STMT_LOOP:
		case PLPGSQL_STMT_FORI:
		case PLPGSQL_STMT_RAISE:
		case PLPGSQL_STMT_CLOSE:
			break;
	}

	return expr;
}

static pc_queryid
profiler_get_dyn_queryid(PLpgSQL_execstate *estate, PLpgSQL_expr *expr, query_params *qparams)
{
	MemoryContext oldcxt;
	Query	   *query;
	RawStmt    *parsetree;
	bool		snapshot_set;
	List	   *parsetree_list;
	PLpgSQL_var result;
	PLpgSQL_type typ;
	char	   *query_string = NULL;
	Oid		   *paramtypes = NULL;
	int			nparams = 0;

	if (qparams)
	{
		paramtypes = qparams->paramtypes;
		nparams = qparams->nparams;
	}

	memset(&result, 0, sizeof(result));
	memset(&typ, 0, sizeof(typ));

	result.dtype = PLPGSQL_DTYPE_VAR;
	result.refname = "*auxstorage*";
	result.datatype = &typ;

	typ.typoid = TEXTOID;
	typ.ttype = PLPGSQL_TTYPE_SCALAR;
	typ.typlen = -1;
	typ.typbyval = false;
	typ.typtype = 'b';

	if (profiler_queryid_mcxt == NULL)
		profiler_queryid_mcxt = AllocSetContextCreate(TopMemoryContext,
													  "plpgsql_check - profiler queryid context",
													  ALLOCSET_DEFAULT_MINSIZE,
													  ALLOCSET_DEFAULT_INITSIZE,
													  ALLOCSET_DEFAULT_MAXSIZE);

	oldcxt = MemoryContextSwitchTo(profiler_queryid_mcxt);
	MemoryContextSwitchTo(oldcxt);

	profiler_plugin2.assign_expr(estate, (PLpgSQL_datum *) &result, expr);

	query_string = TextDatumGetCString(result.value);

	/*
	 * Do basic parsing of the query or queries (this should be safe even if
	 * we are in aborted transaction state!)
	 */
	parsetree_list = pg_parse_query(query_string);


	/* The query can be empty. Returns NOQUERYID is this case. */
	if (!parsetree_list)
	{
		MemoryContextSwitchTo(oldcxt);
		MemoryContextReset(profiler_queryid_mcxt);
		return NOQUERYID;
	}

	/*
	 * There should not be more than one query, silently ignore rather than
	 * error out in that case.
	 */
	if (list_length(parsetree_list) > 1)
	{
		MemoryContextSwitchTo(oldcxt);
		MemoryContextReset(profiler_queryid_mcxt);
		return NOQUERYID;
	}

	/* Run through the raw parsetree and process it. */
	parsetree = (RawStmt *) linitial(parsetree_list);
	snapshot_set = false;

	/*
	 * Set up a snapshot if parse analysis/planning will need one.
	 */
	if (analyze_requires_snapshot(parsetree))
	{
		PushActiveSnapshot(GetTransactionSnapshot());
		snapshot_set = true;
	}

	query = parse_analyze_fixedparams(parsetree, query_string, paramtypes, nparams, NULL);

	if (snapshot_set)
		PopActiveSnapshot();

	MemoryContextSwitchTo(oldcxt);
	MemoryContextReset(profiler_queryid_mcxt);

	return query->queryId;
}

/*
 * Returns result type of already executed (has assigned plan) expression
 */
static bool
get_expr_type(PLpgSQL_expr *expr, Oid *result_type)
{
	if (expr)
	{
		SPIPlanPtr	ptr = expr->plan;

		if (ptr)
		{
			List	   *plan_sources = SPI_plan_get_plan_sources(ptr);

			if (plan_sources && list_length(plan_sources) == 1)
			{
				CachedPlanSource *plan_source;
				TupleDesc	tupdesc;

				plan_source = (CachedPlanSource *) linitial(plan_sources);
				tupdesc = plan_source->resultDesc;

				if (tupdesc->natts == 1)
				{
					*result_type = TupleDescAttr(tupdesc, 0)->atttypid;

					return true;
				}
			}
		}
	}

	return false;
}


/* Return the first queryid found in the given PLpgSQL_stmt, if any. */
static pc_queryid
profiler_get_queryid(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt,
					 bool *has_queryid, query_params **qparams)
{
	PLpgSQL_expr *expr;
	bool		dynamic;
	List	   *params;
	List	   *plan_sources;

	expr = profiler_get_expr(stmt, &dynamic, &params);
	*has_queryid = (expr != NULL);

	/* fast leaving, when expression has not assigned plan */
	if (!expr || !expr->plan)
		return NOQUERYID;

	if (dynamic)
	{
		Assert(expr);

		if (params && !*qparams)
		{
			query_params *qps = NULL;
			int			nparams = list_length(params);
			int			paramno = 0;
			MemoryContext oldcxt;
			ListCell   *lc;

			/* build array of Oid used like dynamic query parameters */
			oldcxt = MemoryContextSwitchTo(profiler_mcxt);
			qps = (query_params *) palloc(sizeof(Oid) * nparams + sizeof(int));
			MemoryContextSwitchTo(oldcxt);

			foreach(lc, params)
			{
				PLpgSQL_expr *param_expr = (PLpgSQL_expr *) lfirst(lc);

				if (!get_expr_type(param_expr, &qps->paramtypes[paramno++]))
				{
					free(qps);
					return NOQUERYID;
				}
			}

			qps->nparams = nparams;
			*qparams = qps;
		}

		return profiler_get_dyn_queryid(estate, expr, *qparams);
	}

	plan_sources = SPI_plan_get_plan_sources(expr->plan);

	if (plan_sources)
	{
		CachedPlanSource *plan_source = (CachedPlanSource *) linitial(plan_sources);

		if (plan_source->query_list)
		{
			Query	   *q = linitial_node(Query, plan_source->query_list);

			return q->queryId;
		}
	}

	return NOQUERYID;
}

/*
 * Generate simple queryid  for testing purpose.
 * DO NOT USE IN PRODUCTION.
 */
static void
profiler_fake_queryid_hook(ParseState *pstate, Query *query, JumbleState *jstate)
{
	(void) jstate;
	(void) pstate;

	Assert(query->queryId == NOQUERYID);

	query->queryId = query->commandType;
}

/*
 * Prepare tuplestore with function profile
 *
 */
void
plpgsql_check_iterate_over_profile(plpgsql_check_info *cinfo,
								   profiler_stmt_walker_mode mode,
								   plpgsql_check_result_info *ri,
								   coverage_state *cs)
{
	LOCAL_FCINFO(fake_fcinfo, 0);
	PLpgSQL_function *func;
	FmgrInfo	flinfo;
	TriggerData trigdata;
	EventTriggerData etrigdata;
	Trigger		tg_trigger;
	ReturnSetInfo rsinfo;
	bool		fake_rtd;
	profiler_info pinfo;
	profiler_stmt_chunk *first_chunk = NULL;
	profiler_iterator pi;
	volatile bool unlock_mutex = false;
	bool		shared_chunks;
	profiler_stmt_walker_options opts;

	memset(&opts, 0, sizeof(profiler_stmt_walker_options));

	memset(&pi, 0, sizeof(profiler_iterator));
	pi.key.fn_oid = cinfo->fn_oid;
	pi.key.db_oid = MyDatabaseId;
	pi.key.fn_xmin = HeapTupleHeaderGetRawXmin(cinfo->proctuple->t_data);
	pi.key.fn_tid = cinfo->proctuple->t_self;
	pi.key.chunk_num = 1;
	pi.ri = ri;

	/* try to find first chunk in shared (or local) memory */
	if (shared_profiler_chunks_HashTable)
	{
		LWLockAcquire(profiler_ss->lock, LW_SHARED);
		pi.chunks = shared_profiler_chunks_HashTable;
		shared_chunks = true;
	}
	else
	{
		pi.chunks = profiler_chunks_HashTable;
		shared_chunks = false;
	}

	pi.current_chunk = first_chunk = (profiler_stmt_chunk *) hash_search(pi.chunks,
																		 (void *) &pi.key,
																		 HASH_FIND,
																		 NULL);

	PG_TRY();
	{
		if (shared_chunks && first_chunk)
		{
			SpinLockAcquire(&first_chunk->mutex);
			unlock_mutex = true;
		}

		plpgsql_check_setup_fcinfo(cinfo,
								   &flinfo,
								   fake_fcinfo,
								   &rsinfo,
								   &trigdata,
								   &etrigdata,
								   &tg_trigger,
								   &fake_rtd);

		func = plpgsql_check__compile_p(fake_fcinfo, false);

		opts.stmtid_map = plpgsql_check_get_stmtid_map(func);
		opts.stmts_info = plpgsql_check_get_stmts_info(func);

		opts.pi = &pi;
		opts.cs = cs;

		pinfo.func = func;
		pinfo.nstatements = 0;
		pinfo.stmts = NULL;

		profiler_stmt_walker(&pinfo, mode, (PLpgSQL_stmt *) func->action, NULL, NULL, 1, &opts);

		pfree(opts.stmtid_map);
		pfree(opts.stmts_info);

	}
	PG_CATCH();
	{
		if (unlock_mutex)
			SpinLockRelease(&first_chunk->mutex);

		PG_RE_THROW();
	}
	PG_END_TRY();

	if (unlock_mutex)
		SpinLockRelease(&first_chunk->mutex);

	if (shared_chunks)
		LWLockRelease(profiler_ss->lock);
}

/*
 * Prepare tuplestore with function profile
 *
 */
void
plpgsql_check_profiler_show_profile(plpgsql_check_result_info *ri,
									plpgsql_check_info *cinfo)
{
	profiler_hashkey hk;
	bool		found;
	HTAB	   *chunks;
	bool		shared_chunks;
	volatile profiler_stmt_chunk *first_chunk = NULL;
	volatile bool unlock_mutex = false;

	/* ensure correct complete content of hash key */
	memset(&hk, 0, sizeof(profiler_hashkey));
	hk.fn_oid = cinfo->fn_oid;
	hk.db_oid = MyDatabaseId;
	hk.fn_xmin = HeapTupleHeaderGetRawXmin(cinfo->proctuple->t_data);
	hk.fn_tid = cinfo->proctuple->t_self;
	hk.chunk_num = 1;

	/* try to find first chunk in shared (or local) memory */
	if (shared_profiler_chunks_HashTable)
	{
		LWLockAcquire(profiler_ss->lock, LW_SHARED);
		chunks = shared_profiler_chunks_HashTable;
		shared_chunks = true;
	}
	else
	{
		chunks = profiler_chunks_HashTable;
		shared_chunks = false;
	}

	PG_TRY();
	{
		char	   *prosrc = cinfo->src;
		profiler_stmt_chunk *chunk = NULL;
		int			lineno = 1;
		int			current_statement = 0;

		chunk = (profiler_stmt_chunk *) hash_search(chunks,
													(void *) &hk,
													HASH_FIND,
													&found);

		if (shared_chunks && chunk)
		{
			first_chunk = chunk;
			SpinLockAcquire(&first_chunk->mutex);
			unlock_mutex = true;
		}

		/* iterate over source code rows */
		while (*prosrc)
		{
			char	   *lineend = NULL;
			char	   *linebeg = NULL;

			int			stmt_lineno = -1;
			int64		us_total = 0;
			int64		exec_count = 0;
			int64		exec_count_err = 0;
			Datum		queryids_array = (Datum) 0;
			Datum		max_time_array = (Datum) 0;
			Datum		processed_rows_array = (Datum) 0;
			int			cmds_on_row = 0;

			lineend = prosrc;
			linebeg = prosrc;

			/* find lineend */
			while (*lineend != '\0' && *lineend != '\n')
				lineend += 1;

			if (*lineend == '\n')
			{
				*lineend = '\0';
				prosrc = lineend + 1;
			}
			else
				prosrc = lineend;

			if (chunk)
			{
				ArrayBuildState *queryids_abs = NULL;
				ArrayBuildState *max_time_abs = NULL;
				ArrayBuildState *processed_rows_abs = NULL;
				int			queryids_on_row = 0;

				queryids_abs = initArrayResult(INT8OID, CurrentMemoryContext, true);
				max_time_abs = initArrayResult(FLOAT8OID, CurrentMemoryContext, true);
				processed_rows_abs = initArrayResult(INT8OID, CurrentMemoryContext, true);

				/* process all statements on this line */
				for (;;)
				{
					/* ensure so  access to chunks is correct */
					if (current_statement >= STATEMENTS_PER_CHUNK)
					{
						hk.chunk_num += 1;

						chunk = (profiler_stmt_chunk *) hash_search(chunks,
																	(void *) &hk,
																	HASH_FIND,
																	&found);

						if (!found)
						{
							chunk = NULL;
							break;
						}

						current_statement = 0;
					}

					Assert(chunk != NULL);

					/* skip invisible statements if any */
					if (chunk->stmts[current_statement].lineno < lineno)
					{
						current_statement += 1;
						continue;
					}
					else if (chunk->stmts[current_statement].lineno == lineno)
					{
						profiler_stmt_reduced *prstmt = &chunk->stmts[current_statement];

						us_total += prstmt->us_total;
						exec_count += prstmt->exec_count;
						exec_count_err += prstmt->exec_count_err;

						stmt_lineno = lineno;

						if (prstmt->has_queryid)
						{
							if (prstmt->queryid != NOQUERYID)
							{
								queryids_abs = accumArrayResult(queryids_abs,
																Int64GetDatum((int64) prstmt->queryid),
																prstmt->queryid == NOQUERYID,
																INT8OID,
																CurrentMemoryContext);
								queryids_on_row += 1;
							}
						}

						max_time_abs = accumArrayResult(max_time_abs,
														Float8GetDatum(prstmt->us_max / 1000.0), false,
														FLOAT8OID,
														CurrentMemoryContext);

						processed_rows_abs = accumArrayResult(processed_rows_abs,
															  Int64GetDatum(prstmt->rows), false,
															  INT8OID,
															  CurrentMemoryContext);
						cmds_on_row += 1;
						current_statement += 1;
						continue;
					}
					else
						break;
				}

				if (queryids_on_row > 0)
					queryids_array = makeArrayResult(queryids_abs, CurrentMemoryContext);

				if (cmds_on_row > 0)
				{
					max_time_array = makeArrayResult(max_time_abs, CurrentMemoryContext);
					processed_rows_array = makeArrayResult(processed_rows_abs, CurrentMemoryContext);
				}
			}

			plpgsql_check_put_profile(ri,
									  queryids_array,
									  lineno,
									  stmt_lineno,
									  cmds_on_row,
									  exec_count,
									  exec_count_err,
									  us_total,
									  max_time_array,
									  processed_rows_array,
									  (char *) linebeg);

			lineno += 1;
		}
	}
	PG_CATCH();
	{
		if (unlock_mutex)
			SpinLockRelease(&first_chunk->mutex);
		PG_RE_THROW();
	}
	PG_END_TRY();

	if (unlock_mutex)
		SpinLockRelease(&first_chunk->mutex);

	if (shared_chunks)
		LWLockRelease(profiler_ss->lock);
}

/*
 * Try to search profile pattern for function. Creates profile pattern when
 * it doesn't exists.
 */
static void
profiler_func_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, void **plugin2_info)
{
	if (plpgsql_check_profiler && OidIsValid(func->fn_oid))
	{
		profiler_info *pinfo;

		pinfo = palloc0(sizeof(profiler_info));
		pinfo->nstatements = func->nstatements;
		pinfo->stmts = palloc0(func->nstatements * sizeof(profiler_stmt));

		INSTR_TIME_SET_CURRENT(pinfo->start_time);

		pinfo->func = func;

		*plugin2_info = pinfo;
	}
}

static void
_profiler_func_end(profiler_info *pinfo, Oid fn_oid, bool is_aborted)
{
	int			entry_stmtid;
	instr_time	end_time;
	uint64		elapsed;
	profiler_stmt_walker_options opts;
	int		   *stmtid_map;

	Assert(pinfo);
	Assert(pinfo->func);
	Assert(pinfo->func->fn_oid == fn_oid);

	entry_stmtid = pinfo->func->action->stmtid - 1;

	memset(&opts, 0, sizeof(profiler_stmt_walker_options));

	INSTR_TIME_SET_CURRENT(end_time);
	INSTR_TIME_SUBTRACT(end_time, pinfo->start_time);

	elapsed = INSTR_TIME_GET_MICROSEC(end_time);

	if (pinfo->stmts[entry_stmtid].exec_count == 0)
	{
		pinfo->stmts[entry_stmtid].exec_count = 1;
		pinfo->stmts[entry_stmtid].exec_count_err = is_aborted ? 1 : 0;
		pinfo->stmts[entry_stmtid].us_total = elapsed;
		pinfo->stmts[entry_stmtid].us_max = elapsed;
	}

	stmtid_map = plpgsql_check_get_current_stmtid_map();

	opts.stmts_info = plpgsql_check_get_current_stmts_info();
	opts.stmtid_map = stmtid_map;

	/* finalize profile - get result profile */
	profiler_stmt_walker(pinfo, PLPGSQL_CHECK_STMT_WALKER_COUNT_EXEC_TIME,
						 (PLpgSQL_stmt *) pinfo->func->action, NULL, NULL, 1,
						 &opts);

	update_persistent_profile(pinfo, pinfo->func, stmtid_map);
	update_persistent_fstats(pinfo->func, elapsed);
}

static void
profiler_func_end(PLpgSQL_execstate *estate,
				  PLpgSQL_function *func,
				  void **plugin2_info)
{
	profiler_info *pinfo = *plugin2_info;

	if (!pinfo)
		return;

	Assert(pinfo->func == func);

	_profiler_func_end(pinfo, func->fn_oid, false);
}

static void
profiler_func_end_aborted(Oid fn_oid, void **plugin2_info)
{
	profiler_info *pinfo = *plugin2_info;

	if (!pinfo)
		return;

	_profiler_func_end(pinfo, fn_oid, true);
}

static void
profiler_stmt_beg(PLpgSQL_execstate *estate,
				  PLpgSQL_stmt *stmt,
				  void **plugin2_info)
{
	profiler_info *pinfo = (profiler_info *) *plugin2_info;

	if (pinfo)
	{
		INSTR_TIME_SET_CURRENT(pinfo->stmts[stmt->stmtid - 1].start_time);
	}
}

static void
_profiler_stmt_end(profiler_stmt *pstmt, bool is_aborted)
{
	instr_time	end_time;
	uint64		elapsed;
	instr_time	end_time2;

	INSTR_TIME_SET_CURRENT(end_time);
	end_time2 = end_time;
	INSTR_TIME_ACCUM_DIFF(pstmt->total, end_time, pstmt->start_time);

	INSTR_TIME_SUBTRACT(end_time2, pstmt->start_time);
	elapsed = INSTR_TIME_GET_MICROSEC(end_time2);

	if (elapsed > pstmt->us_max)
		pstmt->us_max = elapsed;

	pstmt->us_total = INSTR_TIME_GET_MICROSEC(pstmt->total);
	pstmt->exec_count_err += is_aborted ? 1 : 0;
	pstmt->exec_count++;
}


/*
 * Cleaning mode is used for closing unfinished statements after an exception.
 */
static void
profiler_stmt_end(PLpgSQL_execstate *estate,
				  PLpgSQL_stmt *stmt,
				  void **plugin2_info)
{
	profiler_info *pinfo = *plugin2_info;

	if (pinfo)
	{
		profiler_stmt *pstmt = &pinfo->stmts[stmt->stmtid - 1];

		/*
		 * We can get query id only if stmt_end is not executed in cleaning
		 * mode, because we need to execute expression
		 */
		if (pstmt->queryid == NOQUERYID)
			pstmt->queryid = profiler_get_queryid(estate, stmt,
												  &pstmt->has_queryid,
												  &pstmt->qparams);

		_profiler_stmt_end(pstmt, false);
	}
}

static void
profiler_stmt_end_aborted(Oid fn_oid, int stmtid, void **plugin2_info)
{
	profiler_info *pinfo = *plugin2_info;

	if (pinfo)
	{
		profiler_stmt *pstmt = &pinfo->stmts[stmtid - 1];

		_profiler_stmt_end(pstmt, true);
	}
}

void
plpgsql_check_profiler_iterate_over_all_profiles(plpgsql_check_result_info *ri)
{
	HASH_SEQ_STATUS seqstatus;
	fstats	   *fstats_item;

	HTAB	   *fstats_ht;
	bool		htab_is_shared;

	/* try to find first chunk in shared (or local) memory */
	if (shared_fstats_HashTable)
	{
		LWLockAcquire(profiler_ss->fstats_lock, LW_SHARED);
		fstats_ht = shared_fstats_HashTable;
		htab_is_shared = true;
	}
	else
	{
		fstats_ht = fstats_HashTable;
		htab_is_shared = false;
	}

	hash_seq_init(&seqstatus, fstats_ht);

	while ((fstats_item = (fstats *) hash_seq_search(&seqstatus)) != NULL)
	{
		Oid			fn_oid,
					db_oid;
		uint64		exec_count,
					exec_count_err,
					total_time,
					min_time,
					max_time;

		float8		total_time_xx;
		HeapTuple	tp;

		if (htab_is_shared)
			SpinLockAcquire(&fstats_item->mutex);

		fn_oid = fstats_item->key.fn_oid;
		db_oid = fstats_item->key.db_oid;
		exec_count = fstats_item->exec_count;
		exec_count_err = fstats_item->exec_count_err;
		total_time = fstats_item->total_time;
		total_time_xx = fstats_item->total_time_xx;
		min_time = fstats_item->min_time;
		max_time = fstats_item->max_time;

		if (htab_is_shared)
			SpinLockRelease(&fstats_item->mutex);

		/*
		 * only function's statistics for current database can be displayed
		 * here, Oid of functions from other databases has unassigned oids to
		 * current system catalogue.
		 */
		if (db_oid != MyDatabaseId)
			continue;

		/* check if function has name */
		tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(fn_oid));
		if (!HeapTupleIsValid(tp))
			continue;

		ReleaseSysCache(tp);

		plpgsql_check_put_profiler_functions_all_tb(ri,
													fn_oid,
													exec_count,
													exec_count_err,
													(double) total_time,
													ceil(total_time / ((double) exec_count)),
													ceil(sqrt(total_time_xx / exec_count)),
													(double) min_time,
													(double) max_time);
	}

	if (htab_is_shared)
		LWLockRelease(profiler_ss->fstats_lock);
}

/*
 * Register plpgsql plugin2 for profiler
 */
void
plpgsql_check_profiler_init(void)
{
	plpgsql_check_register_pldbgapi2_plugin(&profiler_plugin2);
}
