From 2bddf33d7e6d5152c8097f35428bd441f8b57287 Mon Sep 17 00:00:00 2001 From: Marko Tiikkaja Date: Wed, 4 Jun 2014 20:53:10 +0200 Subject: [PATCH] Initial commit --- Makefile | 20 ++ comm.c | 670 ++++++++++++++++++++++++++++++++++ func_iter.c | 147 ++++++++ pgcov--1.0.sql | 17 + pgcov.c | 947 +++++++++++++++++++++++++++++++++++++++++++++++++ pgcov.control | 5 + pgcov.h | 153 ++++++++ 7 files changed, 1959 insertions(+) create mode 100644 Makefile create mode 100644 comm.c create mode 100644 func_iter.c create mode 100644 pgcov--1.0.sql create mode 100644 pgcov.c create mode 100644 pgcov.control create mode 100644 pgcov.h diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4ed4185 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +MODULE_big = pgcov +OBJS = comm.o pgcov.o func_iter.o + +EXTENSION = pgcov +DATA = pgcov--1.0.sql + +REGRESS = gcov_guts function_line_info gcov + +ifdef NO_PGXS +# Needed to locate plpgsql.h pre-9.2 +PG_CPPFLAGS += -I\$(top_srcdir)/src/pl/plpgsql/src/ +subdir = contrib/pgcov +top_builddir = ../.. +include $(top_builddir)/src/Makefile.Global +include $(top_srcdir)/contrib/contrib-global.mk +else +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +endif diff --git a/comm.c b/comm.c new file mode 100644 index 0000000..faa6ab0 --- /dev/null +++ b/comm.c @@ -0,0 +1,670 @@ +/* + * comm.c: functions for communicating between a backend and a frontend + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pgcov.h" + +#include "miscadmin.h" +#include "lib/stringinfo.h" +#include "nodes/pg_list.h" +#include "utils/memutils.h" + + +static MemoryContext pgcov_protocol_parse_mcxt = NULL; + + +const char* hello_magic = "\x7A\x99\xBC\x01"; + + +static void init_pgcovNetworkConn(pgcovNetworkConn *conn, int sockfd); + +static void nw_append_string(pgcovNetworkConn *conn, const char *str); +static void nw_append_binary(pgcovNetworkConn *conn, const char *str, int len); +static void nw_replace_uint32(char *ptr, uint32 value); +static void nw_append_uint32(pgcovNetworkConn *conn, uint32 value); +static void nw_append_int32(pgcovNetworkConn *conn, int32 value); +static void nw_append_byte(pgcovNetworkConn *conn, uint8_t value); +static void nw_append_msg(pgcovNetworkConn *conn, pgcovProtocolMessageType msg); +static void nw_flush_msg(pgcovNetworkConn *conn); +static void nw_flush_buffer(pgcovNetworkConn *conn); +static void nw_shutdown(pgcovNetworkConn *conn); + +static uint32 nw_peek_uint32(const char *ptr); +static bool nw_buf_has_message(pgcovNetworkConn *conn); + +static void pgcov_nest_accept(pgcovNest *nest); +static void pgcov_nest_read_from_worker(pgcovNest *nest, pgcovWorker *worker); +static void pgcov_nest_shutdown(pgcovNest *nest); + + +static void +init_pgcovNetworkConn(pgcovNetworkConn *conn, int sockfd) +{ + initStringInfo(&conn->sendbuf); + enlargeStringInfo(&conn->sendbuf, 1024); + conn->sockfd = sockfd; +} + +static void +nw_append_string(pgcovNetworkConn *conn, const char *str) +{ + if (str != NULL) + { + int len = strlen(str); + nw_append_uint32(conn, (uint32) len); + nw_append_binary(conn, str, len + 1); + } + else + nw_append_uint32(conn, 0x80000001); +} + +static void +nw_append_binary(pgcovNetworkConn *conn, const char *str, int len) +{ + appendBinaryStringInfo(&conn->sendbuf, str, len); +} + +static void +nw_replace_uint32(char *ptr, uint32 value) +{ + unsigned char *b = (unsigned char *) ptr; + b[0] = (value & 0xFF000000) >> 24; + b[1] = (value & 0x00FF0000) >> 16; + b[2] = (value & 0x0000FF00) >> 8; + b[3] = (value & 0x000000FF); +} + +static void +nw_append_uint32(pgcovNetworkConn *conn, uint32 value) +{ + char b[4]; + nw_replace_uint32(b, value); + return nw_append_binary(conn, b, 4); +} + +static void +nw_append_int32(pgcovNetworkConn *conn, int32 value) +{ + return nw_append_uint32(conn, (uint32) value); +} + +static void +nw_append_byte(pgcovNetworkConn *conn, uint8_t value) +{ + appendStringInfoCharMacro(&conn->sendbuf, value); +} + +static void +nw_append_msg(pgcovNetworkConn *conn, pgcovProtocolMessageType msg) +{ + nw_append_byte(conn, (uint8_t) msg); + /* reserve 4 bytes for the message length */ + nw_append_uint32(conn, 0); +} + +static void +nw_flush_msg(pgcovNetworkConn *conn) +{ + Assert(conn->sendbuf.len >= 5); + nw_replace_uint32(conn->sendbuf.data + 1, conn->sendbuf.len - 1); + nw_flush_buffer(conn); +} + +static void +nw_flush_buffer(pgcovNetworkConn *conn) +{ + int i; + int sent; + int ret; + + Assert(conn->sendbuf.len > 0); + + sent = 0; + for (i = 0; i < 5; i++) + { + /* TODO: implement timeout here */ + ret = write(conn->sockfd, conn->sendbuf.data + sent, conn->sendbuf.len - sent); + if (ret == -1 && errno == EINTR) + continue; + else if (ret == -1) + elog(ERROR, "could not send data to the listener: %s", strerror(errno)); + else if (ret == 0) + elog(FATAL, "connection closed by the listener"); + + sent += ret; + if (sent == conn->sendbuf.len) + { + resetStringInfo(&conn->sendbuf); + return; + } + } + + elog(FATAL, "could not flush %d bytes of data after 5 attempts", conn->sendbuf.len); +} + +static void +nw_client_done(pgcovNetworkConn *conn) +{ + Assert(conn->sendbuf.len == 0); + + /* + * Send the DONE message and give the "nest" process a chance to close the + * connection. This should ensure that we don't lose messages under normal + * operation. + */ + nw_append_msg(conn, PGCOV_MSG_DONE); + nw_flush_msg(conn); + + for (;;) + { + int ret; + struct timeval tv; + fd_set readfds; + char buf; + + FD_ZERO(&readfds); + FD_SET(conn->sockfd, &readfds); + + tv.tv_sec = 1; + tv.tv_usec = 0; + ret = select(conn->sockfd + 1, &readfds, NULL, NULL, &tv); + if (ret == -1 && errno != EINTR) + elog(ERROR, "select() failed: %s", strerror(errno)); + else if (ret == -1) + { + CHECK_FOR_INTERRUPTS(); + continue; + } + else if (ret == 0) + { + elog(WARNING, "connection not closed by the server after PGCOV_MSG_DONE"); + break; + } + + ret = recv(conn->sockfd, &buf, 1, 0); + if (ret == -1 && errno == EINTR) + { + CHECK_FOR_INTERRUPTS(); + continue; + } + else if (ret != 0) + { + Assert(ret == -1); + elog(WARNING, "recv() failed: %d, %s", ret, strerror(errno)); + } + /* closed, we're done */ + break; + } + nw_shutdown(conn); +} + +static void +nw_shutdown(pgcovNetworkConn *conn) +{ + if (conn->sendbuf.data != NULL) + pfree(conn->sendbuf.data); + (void) shutdown(conn->sockfd, SHUT_RDWR); + close(conn->sockfd); +} + + +static uint32 +nw_peek_uint32(const char *ptr) +{ + unsigned char *b = (unsigned char *) ptr; + uint32 value; + + value = ((uint32) b[0]) << 24; + value |= ((uint32) b[1]) << 16; + value |= ((uint32) b[2]) << 8; + value |= ((uint32) b[3]); + return value; +} + +static bool +nw_buf_has_message(pgcovNetworkConn *conn) +{ + if (conn->rcvbuf.len < 5) + return false; + return conn->rcvbuf.len >= nw_peek_uint32(conn->rcvbuf.data + 1); +} + +/* + * Discards the first message in conn->rcvbuf. A message must exist. + */ +static void +nw_buf_discard_message(pgcovNetworkConn *conn) +{ + Assert(nw_buf_has_message(conn)); + int32 msglen = (int32) nw_peek_uint32(conn->rcvbuf.data + 1) + 1; + Assert(conn->rcvbuf.len >= msglen); + int32 remaining = conn->rcvbuf.len - msglen; + if (remaining > 0) + { + memmove(conn->rcvbuf.data, conn->rcvbuf.data + msglen, remaining); + conn->rcvbuf.len -= msglen; + } + else + conn->rcvbuf.len = 0; +} + +static PGCOV_MESSAGE_PARSE_FUNC(nw_parse_coverage_report); + +static void +nw_parse_message(pgcovWorker *worker) +{ + pgcovNetworkConn *conn = &worker->buf; + + while (nw_buf_has_message(conn)) + { + switch (conn->rcvbuf.data[0]) + { + case PGCOV_MSG_COVERAGE_REPORT: + PGCOV_PARSE(nw_parse_coverage_report); + break; + + case PGCOV_MSG_DONE: + worker->done = true; + return; + + default: + elog(ERROR, "unrecognized message type %d", conn->rcvbuf.data[0]); + } + } +} + +static +PGCOV_MESSAGE_PARSE_FUNC(nw_parse_coverage_report) +{ + Oid fnoid; + char *fnsignature; + int32 ncalls; + char *prosrc; + List *lines; + int32 num_lines; + int32 i; + + PGCOV_PARSE_UINT32(&fnoid); /* TODO dboid */ + PGCOV_PARSE_STRING(&fnsignature); + PGCOV_PARSE_INT32(&ncalls); + PGCOV_PARSE_UINT32(&fnoid); + PGCOV_PARSE_STRING(&prosrc); + PGCOV_PARSE_INT32(&num_lines); + lines = NIL; + for (i = 0; i < num_lines; i++) + { + pgcovFunctionLine *line = (pgcovFunctionLine *) palloc(sizeof(pgcovFunctionLine)); + PGCOV_PARSE_INT32(&line->lineno); + PGCOV_PARSE_INT32(&line->num_executed); + lines = lappend(lines, line); + } + /* process the results */ + pgcov_function_coverage_sfunc(fnoid, fnsignature, ncalls, prosrc, lines); + PGCOV_END_PARSE(); +} + +/* + * Starts listening for incoming connections. + */ +void +pgcov_start_listener(pgcovNest *nest) +{ + int ret; + struct addrinfo hints; + struct addrinfo *res; + int sockfd; + struct sockaddr_in localaddr; + socklen_t addrlen = sizeof(localaddr); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST | AI_NUMERICSERV; + + if ((ret = getaddrinfo("127.0.0.1", "0", &hints, &res)) != 0) + elog(FATAL, "getaddrinfo() failed: %s", strerror(ret)); + + sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sockfd < 0) + elog(FATAL, "could not create a socket: %s", strerror(errno)); + + if (bind(sockfd, res->ai_addr, res->ai_addrlen) < 0) + { + int bind_error = errno; + close(sockfd); + freeaddrinfo(res); + elog(FATAL, "bind() failed: %s", strerror(bind_error)); + } + freeaddrinfo(res); + + if (listen(sockfd, 15) < 0) + { + int listen_error = errno; + close(sockfd); + elog(FATAL, "listen() failed: %s", strerror(listen_error)); + } + + if (getsockname(sockfd, (struct sockaddr *) &localaddr, &addrlen) < 0) + elog(FATAL, "getsockname() failed: %s", strerror(errno)); + + Assert(sizeof(nest->entrance) == MAX_ENTRANCE_SIZE); + ret = snprintf(nest->entrance, MAX_ENTRANCE_SIZE, + "%hu", ntohs(localaddr.sin_port)); + if (ret < 0 || ret >= MAX_ENTRANCE_SIZE) + elog(FATAL, "snprintf() failed: %s", strerror(errno)); + + elog(DEBUG1, "nest entrance: %s", nest->entrance); + + nest->lsockfd = sockfd; + + { + int i; + + nest->max_workers = 15; /* TODO */ + nest->workers = (pgcovWorker *) palloc(sizeof(pgcovWorker) * nest->max_workers); + nest->nworkers = 0; + for (i = 0; i < nest->max_workers; i++) + { + nest->workers[i].free = true; + nest->workers[i].buf.sockfd = -1; + } + elog(DEBUG1, "allocated space for %d workers", nest->max_workers); + } + + /* + * Init a protocol parse context. This will be reset after parsing any + * single message from a backend. + */ + if (!pgcov_protocol_parse_mcxt) + { + pgcov_protocol_parse_mcxt = + AllocSetContextCreate(TopMemoryContext, + "pgcov listener aggregated data memory context", + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + } + else + MemoryContextReset(pgcov_protocol_parse_mcxt); +} + +void +pgcov_stop_listener(pgcovNest *nest) +{ + pgcov_nest_shutdown(nest); + if (pgcov_protocol_parse_mcxt) + { + MemoryContextDelete(pgcov_protocol_parse_mcxt); + pgcov_protocol_parse_mcxt = NULL; + } +} + +static void +pgcov_nest_accept(pgcovNest *nest) +{ + int i; + int sockfd; + struct sockaddr_storage their_addr; + socklen_t addr_size; + pgcovWorker *worker; + + sockfd = accept(nest->lsockfd, (struct sockaddr *) &their_addr, &addr_size); + if (sockfd == -1 && errno == EINTR) + return; + else if (sockfd == -1) + { + /* pgcov_nest_shutdown() will likely overwrite our errno */ + int accept_error = errno; + pgcov_nest_shutdown(nest); + elog(ERROR, "could not accept(): %s", strerror(accept_error)); + } + + if (nest->nworkers + 1 >= nest->max_workers) + { + elog(WARNING, "no more available worker slots in nest (%d workers, max %d)", nest->nworkers, nest->max_workers); + close(sockfd); + return; + } + + /* find a free slot */ + worker = NULL; + for (i = 0; i < nest->max_workers; i++) + { + if (nest->workers[i].free) + { + worker = &nest->workers[i]; + break; + } + } + /* should definitely not happen */ + if (!worker) + elog(ERROR, "could not find a free worker slot"); + init_pgcovNetworkConn(&worker->buf, sockfd); + nest->nworkers++; + worker->free = false; + worker->done = false; + initStringInfo(&nest->workers[i].buf.rcvbuf); +} + +/* + * Return false on errors, true otherwise (even if there was not a complete + * message in the buffer). + */ +static bool +pgcov_parse_worker_data(pgcovWorker *worker) +{ + bool success = true; + MemoryContext currcxt; + + /* restore the memory context if we get a parse error */ + currcxt = CurrentMemoryContext; + PG_TRY(); + { + nw_parse_message(worker); + } + PG_CATCH(); + { + FlushErrorState(); + + MemoryContextSwitchTo(currcxt); + success = false; + } + PG_END_TRY(); + + return success; +} + +static void +pgcov_nest_read_from_worker(pgcovNest *nest, pgcovWorker *worker) +{ + int ret; + char buf[2048]; + + ret = read(worker->buf.sockfd, buf, sizeof(buf)); + if (ret == -1 && errno == EINTR) + return; + else if (ret == -1) + { + elog(WARNING, "read error: %s", strerror(errno)); + goto done; + } + else if (ret == 0) + goto done; + + appendBinaryStringInfo(&worker->buf.rcvbuf, buf, ret); + + if (!pgcov_parse_worker_data(worker)) + goto done; + + if (worker->done) + goto done; + else + return; + +done: + /* this worker is done, clean up */ + pfree(worker->buf.rcvbuf.data); + close(worker->buf.sockfd); + worker->buf.sockfd = -1; + worker->free = true; + nest->nworkers--; +} + +static void +pgcov_nest_shutdown(pgcovNest *nest) +{ + int i; + + for (i = 0; i < nest->max_workers; i++) + { + if (nest->workers[i].free) + continue; + shutdown(nest->workers[i].buf.sockfd, SHUT_RDWR); + close(nest->workers[i].buf.sockfd); + + /* + * We don't need to clear the network buffer; that'll go away once the + * memory context we're in is cleared. + */ + } + nest->nworkers = 0; + close(nest->lsockfd); +} + +void +pgcov_gather_information(pgcovNest *nest) +{ + for (;;) + { + fd_set readfds; + int i; + int nworkers; + int maxsockfd; + int ret; + struct timeval tv; + + FD_ZERO(&readfds); + FD_SET(nest->lsockfd, &readfds); + maxsockfd = nest->lsockfd; + + nworkers = nest->nworkers; + for (i = 0; i < nest->max_workers && nworkers > 0; i++) + { + int wsockfd; + + if (nest->workers[i].free) + continue; + + wsockfd = nest->workers[i].buf.sockfd; + FD_SET(wsockfd, &readfds); + if (wsockfd > maxsockfd) + maxsockfd = wsockfd; + } + + tv.tv_sec = 3; + tv.tv_usec = 0; + ret = select(maxsockfd+1, &readfds, NULL, NULL, &tv); + if (ret == -1 && errno != EINTR) + elog(FATAL, "select() failed: %s", strerror(errno)); + else if (ret == -1 || ret == 0) + { + CHECK_FOR_INTERRUPTS(); + continue; + } + + if (FD_ISSET(nest->lsockfd, &readfds)) + { + pgcov_nest_accept(nest); + ret--; + } + + /* finally, read from the workers */ + for (i = 0; i < nest->max_workers && ret > 0; i++) + { + pgcovWorker *worker = &nest->workers[i]; + if (worker->free) + continue; + if (!FD_ISSET(worker->buf.sockfd, &readfds)) + continue; + pgcov_nest_read_from_worker(nest, worker); + ret--; + } + } +} + +void +pgcov_worker_connect(pgcovNetworkConn *conn, const char *nest_entrance) +{ + int ret; + struct addrinfo hints; + struct addrinfo *ai; + int sockfd; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + if ((ret = getaddrinfo("127.0.0.1", nest_entrance, &hints, &ai)) != 0) + elog(FATAL, "getaddrinfo() failed: %s", strerror(ret)); + + sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sockfd < 0) + elog(FATAL, "could not create a socket: %s", strerror(errno)); + + if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) + { + close(sockfd); + elog(FATAL, "could not connect to %s: %s", "127.0.0.1", strerror(errno)); + } + + init_pgcovNetworkConn(conn, sockfd); + //nw_append_binary(conn, hello_magic, strlen(hello_magic)); +} + +void +pgcov_emit_function_coverage_report(const pgcovStackFrame *fn) +{ + ListCell *lc; + pgcovNetworkConn conn; + char nest_entrance[MAX_ENTRANCE_SIZE]; + + if (!pgcov_get_active_listener(nest_entrance)) + return; + + conn.sockfd = -1; + PG_TRY(); + { + pgcov_worker_connect(&conn, nest_entrance); + nw_append_msg(&conn, PGCOV_MSG_COVERAGE_REPORT); + nw_append_uint32(&conn, (uint32) MyDatabaseId); + nw_append_string(&conn, fn->fnsignature); + nw_append_uint32(&conn, 1); /* XXX ncalls is always 1 currently */ + nw_append_uint32(&conn, fn->fnoid); + nw_append_string(&conn, fn->prosrc); + nw_append_int32(&conn, (int32) list_length(fn->lines)); + foreach(lc, fn->lines) + { + pgcovFunctionLine *line = (pgcovFunctionLine *) lfirst(lc); + nw_append_int32(&conn, line->lineno); + nw_append_int32(&conn, line->num_executed); + } + nw_flush_msg(&conn); + nw_client_done(&conn); + } + PG_CATCH(); + { + if (conn.sockfd != -1) + close(conn.sockfd); + PG_RE_THROW(); + } + PG_END_TRY(); +} diff --git a/func_iter.c b/func_iter.c new file mode 100644 index 0000000..36e800f --- /dev/null +++ b/func_iter.c @@ -0,0 +1,147 @@ +/* + * func_iter.c: + * iterate over all statements of a PL/PgSQL function + */ +#include "postgres.h" +#include "plpgsql.h" + +#include "pgcov.h" + + +static bool fniter_stmt_iterate(PLpgSQL_function *func, + PLpgSQL_stmt *stmt, + fniter_context *context); +static bool fniter_body_iterate(PLpgSQL_function *func, + PLpgSQL_stmt *stmt, + List *body, + fniter_context *context); + +bool +fniter_function_iterate(PLpgSQL_function *func, fniter_context *context) +{ + return fniter_stmt_iterate(func, (PLpgSQL_stmt *) func->action, context); +} + +static bool +fniter_body_iterate(PLpgSQL_function *func, PLpgSQL_stmt *stmt, + List *body, fniter_context *context) +{ + ListCell *lc; + + if (!body) + return false; + + if (context->enter_body && + context->enter_body(func, stmt, body, context->auxiliary)) + return true; + + foreach(lc, body) + { + if (fniter_stmt_iterate(func, (PLpgSQL_stmt *) lfirst(lc), context)) + return true; + } + + if (context->exit_body) + return context->exit_body(func, stmt, body, context->auxiliary); + return false; +} + +static bool +fniter_stmt_iterate(PLpgSQL_function *func, PLpgSQL_stmt *stmt, fniter_context *context) +{ + if (context->enter_stmt && + context->enter_stmt(func, stmt, context->auxiliary)) + return true; + + switch (stmt->cmd_type) + { + case PLPGSQL_STMT_BLOCK: + { + PLpgSQL_stmt_block *stmtblock = (PLpgSQL_stmt_block *) stmt; + if (fniter_body_iterate(func, stmt, stmtblock->body, context)) + return true; + if (stmtblock->exceptions) + { + ListCell *lc; + + foreach(lc, stmtblock->exceptions->exc_list) + { + if (fniter_body_iterate(func, stmt, ((PLpgSQL_exception *) lfirst(lc))->action, context)) + return true; + } + } + } + break; + case PLPGSQL_STMT_IF: + { + PLpgSQL_stmt_if *ifstmt = (PLpgSQL_stmt_if *) stmt; +#if PG_VERSION_NUM < 90200 + if (fniter_body_iterate(func, stmt, ifstmt->true_body, context)) + return true; + if (fniter_body_iterate(func, stmt, ifstmt->false_body, context)) + return true; +#else + if (fniter_body_iterate(func, stmt, ifstmt->then_body, context)) + return true; + if (ifstmt->elsif_list) + { + ListCell *lc; + + foreach(lc, ifstmt->elsif_list) + { + if (fniter_body_iterate(func, stmt, ((PLpgSQL_if_elsif *) lfirst(lc))->stmts, context)) + return true; + } + } + if (fniter_body_iterate(func, stmt, ifstmt->else_body, context)) + return true; +#endif + } + break; + case PLPGSQL_STMT_LOOP: + if (fniter_body_iterate(func, stmt, ((PLpgSQL_stmt_loop *) stmt)->body, context)) + return true; + break; + case PLPGSQL_STMT_FOREACH_A: + if (fniter_body_iterate(func, stmt, ((PLpgSQL_stmt_foreach_a *) stmt)->body, context)) + return true; + break; + case PLPGSQL_STMT_FORI: + if (fniter_body_iterate(func, stmt, ((PLpgSQL_stmt_fori *) stmt)->body, context)) + return true; + break; + case PLPGSQL_STMT_FORS: + case PLPGSQL_STMT_FORC: + case PLPGSQL_STMT_DYNFORS: + if (fniter_body_iterate(func, stmt, ((PLpgSQL_stmt_forq *) stmt)->body, context)) + return true; + break; + case PLPGSQL_STMT_WHILE: + if (fniter_body_iterate(func, stmt, ((PLpgSQL_stmt_while *) stmt)->body, context)) + return true; + break; + + case PLPGSQL_STMT_ASSIGN: + case PLPGSQL_STMT_EXIT: + case PLPGSQL_STMT_RETURN: + case PLPGSQL_STMT_RETURN_NEXT: + case PLPGSQL_STMT_RETURN_QUERY: + case PLPGSQL_STMT_RAISE: + case PLPGSQL_STMT_EXECSQL: + case PLPGSQL_STMT_GETDIAG: + case PLPGSQL_STMT_OPEN: + case PLPGSQL_STMT_FETCH: + case PLPGSQL_STMT_CLOSE: + case PLPGSQL_STMT_PERFORM: + /* nothing to do */ + break; + + default: + elog(ERROR, "unknown cmd_type %d", stmt->cmd_type); + break; + } + + if (context->exit_stmt) + return context->exit_stmt(func, stmt, context->auxiliary); + return false; +} diff --git a/pgcov--1.0.sql b/pgcov--1.0.sql new file mode 100644 index 0000000..a1a2899 --- /dev/null +++ b/pgcov--1.0.sql @@ -0,0 +1,17 @@ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION pgcov" to load this file. \quit + +CREATE FUNCTION pgcov_listen() RETURNS VOID + AS 'pgcov', 'pgcov_listen' LANGUAGE c; + +CREATE FUNCTION pgcov_called_functions() + RETURNS TABLE (fnsignature text, fnoid oid, ncalls int4, coverage double precision) + AS 'pgcov', 'pgcov_called_functions' LANGUAGE c; + +CREATE FUNCTION pgcov_fn_line_coverage(fnsignature text) + RETURNS TABLE (lineno int4, ncalls int4, src text) + AS 'pgcov', 'pgcov_fn_line_coverage' LANGUAGE c; + +CREATE FUNCTION pgcov_fn_line_coverage_src(fnsignature text) + RETURNS text + AS 'pgcov', 'pgcov_fn_line_coverage_src' LANGUAGE c; diff --git a/pgcov.c b/pgcov.c new file mode 100644 index 0000000..1c647c1 --- /dev/null +++ b/pgcov.c @@ -0,0 +1,947 @@ +#include "postgres.h" + +#include "plpgsql.h" +#include "funcapi.h" +#include "fmgr.h" +#include "access/hash.h" +#include "catalog/pg_type.h" +#include "catalog/pg_proc.h" +#include "utils/syscache.h" +#include "utils/builtins.h" +#include "executor/executor.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "storage/ipc.h" +#include "utils/guc.h" +#include "utils/memutils.h" +#include "miscadmin.h" + +#if PG_VERSION_NUM >= 90300 +#include "access/htup_details.h" +#endif + + +#include "pgcov.h" + + +PG_MODULE_MAGIC; + + +/* ************* * + * shared memory * + * ************* */ + +static shmem_startup_hook_type prev_shmem_startup_hook = NULL; + +typedef struct { + LWLockId lock; + char nest_entrance[MAX_ENTRANCE_SIZE]; +} pgcovSharedState; + +static pgcovSharedState *pgcov = NULL; + + +/* ******* * + * testing * + * ******* */ + +static bool pgcov_test_function_line_info = false; + + +/* ****************** * + * backend-local data * + * ****************** */ + +static List *pgcov_call_stack = NIL; + +/* + * All memory in the call stack should be allocated in this memory context. We + * reset the context every time the stack is completely unwound in order to + * avoid leaking any memory. + */ +static MemoryContext pgcov_call_stack_mctx = NULL; + + +/* *********************************** * + * backend-local data for the listener * + * *********************************** */ + +typedef struct { + Oid dbid; + char *fnsignature; +} pgcovFcHashKey; + +typedef struct { + pgcovFcHashKey key; + + Oid fnoid; + + int32 ncalls; + + char *prosrc; + List *lines; /* list of pgcovFunctionLine */ +} pgcovFunctionCoverage; + +static uint32 pgcov_function_coverage_hashfn(const void *key, Size keysize); +static int pgcov_function_coverage_matchfn(const void *key1, const void *key2, Size keysize); + +/* + * All data gathered by the listener should be kept in this memory context. + * It is created when we start listening and destroyed once we're ready to + * abandon the data, either by listening again or by an explicit call to reset. + */ +MemoryContext pgcov_listener_mcxt = NULL; +static HTAB *pgcov_function_coverage = NULL; + +static void pgcov_init_listener(void); + + +/* ************************* * + * PL/PgSQL plugin callbacks * + * ************************* */ + +static void pgcov_plpgsql_func_beg(PLpgSQL_execstate *estate, + PLpgSQL_function *func); +static void pgcov_plpgsql_stmt_beg(PLpgSQL_execstate *estate, + PLpgSQL_stmt *stmt); + +static PLpgSQL_plugin pgcov_plpgsql_plugin_struct = { + NULL, /* func_setup */ + pgcov_plpgsql_func_beg, + NULL, /* func_end */ + pgcov_plpgsql_stmt_beg, + NULL, /* stmt_end */ +}; + + +/* fmgr hooks */ +static bool pgcov_needs_fmgr_hook(Oid fnoid); +static void pgcov_fmgr_hook(FmgrHookEventType event, FmgrInfo *flinfo, Datum *args); + +static needs_fmgr_hook_type prev_needs_fmgr_hook = NULL; +static fmgr_hook_type prev_fmgr_hook = NULL; + + +/* shared memory manipulation routines */ +#define pgcov_require_shmem() \ + do { \ + if (!pgcov) \ + ereport(ERROR, \ + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), \ + errmsg("shared memory not initialized"), \ + errhint("pgcov must be in shared_preload_libraries"))); \ + } while (0) + +static void pgcov_shmem_startup(void); +static void pgcov_shmem_set_listener(const pgcovNest *nest); +static void pgcov_shmem_clear_listener(const pgcovNest *nest); + +void _PG_init(void); +void _PG_fini(void); + + +/* local functions for fetching information about functions */ +static bool pgcov_get_function_line_info_enter_stmt(PLpgSQL_function *func, + PLpgSQL_stmt *stmt, + void *aux); +static void pgcov_get_function_line_info(pgcovStackFrame *fn, + PLpgSQL_function *func, + const char *prosrc); +static char *get_function_signature(HeapTuple proctup, Form_pg_proc procform, + const char *proname); +static void pgcov_record_stmt_enter(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt); + + +/* + * Module load callback + */ +void +_PG_init(void) +{ + PLpgSQL_plugin **plpgsql_plugin; + + /* must be loaded via shared_preload_libraries */ + if (!process_shared_preload_libraries_in_progress) + return; + + pgcov_call_stack_mctx = + AllocSetContextCreate(TopMemoryContext, + "pgcov call stack memory context", + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + + plpgsql_plugin = (PLpgSQL_plugin **) find_rendezvous_variable("PLpgSQL_plugin"); + *plpgsql_plugin = &pgcov_plpgsql_plugin_struct; + + prev_needs_fmgr_hook = needs_fmgr_hook; + needs_fmgr_hook = pgcov_needs_fmgr_hook; + prev_fmgr_hook = fmgr_hook; + fmgr_hook = pgcov_fmgr_hook; + + /* shared memory init */ + RequestAddinShmemSpace(1024); + RequestAddinLWLocks(1); + + prev_shmem_startup_hook = shmem_startup_hook; + shmem_startup_hook = pgcov_shmem_startup; +} + +/* + * Module unload callback + */ +void +_PG_fini(void) +{ + PLpgSQL_plugin **plpgsql_plugin; + + plpgsql_plugin = (PLpgSQL_plugin **) find_rendezvous_variable("PLpgSQL_plugin"); + *plpgsql_plugin = NULL; + + needs_fmgr_hook = prev_needs_fmgr_hook; + fmgr_hook = prev_fmgr_hook; +} + +static void +pgcov_shmem_startup(void) +{ + bool found; + + if (prev_shmem_startup_hook) + prev_shmem_startup_hook(); + + /* + * Create or attach to the shared memory state, including hash table + */ + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + pgcov = ShmemInitStruct("pgcov", + sizeof(pgcovSharedState), + &found); + + if (!found) + { + /* First time through ... */ + pgcov->lock = LWLockAssign(); + pgcov->nest_entrance[0] = '\0'; + } + + LWLockRelease(AddinShmemInitLock); +} + +static void +pgcov_shmem_set_listener(const pgcovNest *nest) +{ + volatile pgcovSharedState *shmem; + pgcov_require_shmem(); + + LWLockAcquire(pgcov->lock, LW_EXCLUSIVE); + shmem = pgcov; + if (shmem->nest_entrance[0] != '\0') + elog(ERROR, "an active listener already exists"); + memcpy(pgcov->nest_entrance, nest->entrance, MAX_ENTRANCE_SIZE); + LWLockRelease(pgcov->lock); +} + +static void +pgcov_shmem_clear_listener(const pgcovNest *nest) +{ + pgcov_require_shmem(); + + LWLockAcquire(pgcov->lock, LW_EXCLUSIVE); + if (memcmp(pgcov->nest_entrance, nest->entrance, MAX_ENTRANCE_SIZE) != 0) + elog(ERROR, "unexpected entrance %s, was expecting %s", pgcov->nest_entrance, nest->entrance); + pgcov->nest_entrance[0] = '\0'; + LWLockRelease(pgcov->lock); +} + +/* + * Init all the data structures required to track stuff. XXX + */ +static void +pgcov_init_listener(void) +{ + HASHCTL ctl; + int flags; + + pgcov_listener_mcxt = + AllocSetContextCreate(TopMemoryContext, + "pgcov listener aggregated data memory context", + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + + memset(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(pgcovFcHashKey); + ctl.entrysize = sizeof(pgcovFunctionCoverage); + ctl.hash = pgcov_function_coverage_hashfn; + ctl.match = pgcov_function_coverage_matchfn; + /* use our memory context for the hash table */ + ctl.hcxt = pgcov_listener_mcxt; + + flags = HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_CONTEXT; + + pgcov_function_coverage = + hash_create("pgcov_function_coverage_hash_table", 64, &ctl, flags); +} + +static uint32 +pgcov_function_coverage_hashfn(const void *ptr, Size keysize) +{ + pgcovFcHashKey *key = (pgcovFcHashKey *) ptr; + + Assert(keysize == sizeof(pgcovFcHashKey)); + return DatumGetUInt32(hash_any((void *) key->fnsignature, strlen(key->fnsignature))); +} + +static int +pgcov_function_coverage_matchfn(const void *ptr1, const void *ptr2, Size keysize) +{ + pgcovFcHashKey *key1 = (pgcovFcHashKey *) ptr1; + pgcovFcHashKey *key2 = (pgcovFcHashKey *) ptr2; + + Assert(keysize == sizeof(pgcovFcHashKey)); + if (key1->dbid < key2->dbid) + return -1; + else if (key1->dbid > key2->dbid) + return 1; + else + return strcmp(key1->fnsignature, key2->fnsignature); +} + + +Datum pgcov_called_functions(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_called_functions); + +Datum +pgcov_called_functions(PG_FUNCTION_ARGS) +{ + MemoryContext oldcxt; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + Tuplestorestate *tupstore; + TupleDesc tupdesc; + + if (!pgcov_function_coverage) + elog(ERROR, "record some data first (see pgcov_listen())"); + + /* check to see if caller supports us returning a tuplestore */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not " \ + "allowed in this context"))); + + /* switch to long-lived memory context */ + oldcxt = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); + + /* get the requested return tuple description */ + tupdesc = CreateTupleDescCopy(rsinfo->expectedDesc); + if (tupdesc->natts != 4) + elog(ERROR, "unexpected natts %d", tupdesc->natts); + + tupstore = + tuplestore_begin_heap(rsinfo->allowedModes & SFRM_Materialize_Random, + false, work_mem); + + /* walk over the hash table */ + { + HASH_SEQ_STATUS hseq; + pgcovFunctionCoverage *fn; + Datum values[4]; + bool isnull[4] = { false, false, false, true }; + + hash_seq_init(&hseq, pgcov_function_coverage); + while ((fn = (pgcovFunctionCoverage *) hash_seq_search(&hseq)) != NULL) + { + values[0] = CStringGetTextDatum(fn->key.fnsignature); + values[1] = ObjectIdGetDatum(fn->fnoid); + values[2] = Int32GetDatum(fn->ncalls); + tuplestore_putvalues(tupstore, tupdesc, values, isnull); + } + } + + MemoryContextSwitchTo(oldcxt); + + /* let the caller know we're sending back a tuplestore */ + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + + return (Datum) 0; +} + +Datum pgcov_fn_line_coverage(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_fn_line_coverage); + +Datum +pgcov_fn_line_coverage(PG_FUNCTION_ARGS) +{ + MemoryContext oldcxt; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + Tuplestorestate *tupstore; + TupleDesc tupdesc; + char *fnsignature; + + if (!pgcov_function_coverage) + elog(ERROR, "record some data first (see pgcov_listen())"); + + /* check to see if caller supports us returning a tuplestore */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not " \ + "allowed in this context"))); + + /* get a cstring of the argument in the short-lived context */ + fnsignature = TextDatumGetCString(PG_GETARG_DATUM(0)); + + /* .. and then switch to long-lived memory context */ + oldcxt = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); + + /* get the requested return tuple description */ + tupdesc = CreateTupleDescCopy(rsinfo->expectedDesc); + if (tupdesc->natts != 3) + elog(ERROR, "unexpected natts %d", tupdesc->natts); + + tupstore = + tuplestore_begin_heap(rsinfo->allowedModes & SFRM_Materialize_Random, + false, work_mem); + + { + pgcovFunctionCoverage *fn; + bool found; + Datum values[3]; + bool isnull[3] = { false, false, true }; + const pgcovFcHashKey key = { 0, fnsignature }; + ListCell *lc; + + fn = hash_search(pgcov_function_coverage, (void *) &key, HASH_FIND, &found); + if (!found) + elog(ERROR, "could not find function %s", fnsignature); + + foreach(lc, fn->lines) + { + pgcovFunctionLine *line = (pgcovFunctionLine *) lfirst(lc); + values[0] = Int32GetDatum(line->lineno); + values[1] = Int32GetDatum(line->num_executed); + tuplestore_putvalues(tupstore, tupdesc, values, isnull); + } + } + + MemoryContextSwitchTo(oldcxt); + + /* let the caller know we're sending back a tuplestore */ + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + + return (Datum) 0; +} + +Datum pgcov_fn_line_coverage_src(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_fn_line_coverage_src); + +Datum +pgcov_fn_line_coverage_src(PG_FUNCTION_ARGS) +{ + pgcovFunctionCoverage *fn; + bool found; + pgcovFcHashKey key = { 0, NULL }; + + if (!pgcov_function_coverage) + elog(ERROR, "record some data first (see pgcov_listen())"); + + key.fnsignature = TextDatumGetCString(PG_GETARG_DATUM(0)); + + fn = hash_search(pgcov_function_coverage, (void *) &key, HASH_FIND, &found); + if (!found) + elog(ERROR, "could not find function %s", key.fnsignature); + + if (!fn->prosrc) + PG_RETURN_NULL(); + + PG_RETURN_DATUM(CStringGetTextDatum(fn->prosrc)); +} + + +/* + * Takes a newly received function coverage report and incorporates its data + * into the data we've gathered so far. + */ +void +pgcov_function_coverage_sfunc(Oid fnoid, char *fnsignature, int32 ncalls, + char *prosrc, List *lines) +{ + const pgcovFcHashKey key = { 0, fnsignature }; + bool found; + pgcovFunctionCoverage *fn; + MemoryContext oldcxt; + ListCell *lc; + + fn = hash_search(pgcov_function_coverage, (const void *) &key, HASH_ENTER, &found); + if (!found) + { + /* we need to copy the signature out of the parse context */ + fn->key.fnsignature = MemoryContextStrdup(pgcov_listener_mcxt, key.fnsignature); + + fn->prosrc = NULL; + fn->lines = NIL; + goto replace; + } + + /* + * See if the function's source code is still the same. If it's not, + * forget everything we thought we knew about the function and replace it. + * This allows us to still somewhat function if e.g. a test suite replaces + * a function with a mocked version. N.B. we specifically do *not* look at + * the oid of the function. This way we still keep track of a function + * even if it gets replaced via DROP/CREATE function instead of CREATE OR + * REPLACE. + */ + if (strcmp(fn->prosrc, prosrc) == 0) + { + ListCell *lc1, *lc2; + + fn->ncalls += ncalls; + + if (list_length(fn->lines) != list_length(lines)) + elog(ERROR, "line list_length oops"); + + forboth(lc1, fn->lines, lc2, lines) + { + pgcovFunctionLine *old = lfirst(lc1); + pgcovFunctionLine *new = lfirst(lc2); + + if (old->lineno != new->lineno) + elog(ERROR, "lineno oops"); + old->num_executed += new->num_executed; + } + + return; + } + +replace: + /* + * This is either a function we haven't seen before, or the function with + * this signature got replaced with a new definition. Copy all items out + * of the parse context into ours. Also remember to free the previous ones + * if we're replacing or repeatedly redefining functions would result in + * memory leaks. + */ + + oldcxt = MemoryContextSwitchTo(pgcov_listener_mcxt); + fn->fnoid = fnoid; + fn->ncalls = ncalls; + + if (fn->prosrc) + pfree(fn->prosrc); + + if (prosrc) + fn->prosrc = pstrdup(prosrc); + else + fn->prosrc = NULL; + + if (fn->lines) + { + list_free(fn->lines); + fn->lines = NIL; + } + + foreach(lc, lines) + { + pgcovFunctionLine *line = (pgcovFunctionLine *) palloc(sizeof(pgcovFunctionLine)); + memcpy(line, lfirst(lc), sizeof(pgcovFunctionLine)); + fn->lines = lappend(fn->lines, line); + } + MemoryContextSwitchTo(oldcxt); +} + +/* + * Returns true if an active listener exists, false otherwise. If the return + * value is true, "entrance" is populated with information about the entrance. + */ +bool +pgcov_get_active_listener(char entrance[MAX_ENTRANCE_SIZE]) +{ + bool active; + volatile pgcovSharedState *shmem; + + if (!pgcov) + return false; + + LWLockAcquire(pgcov->lock, LW_SHARED); + shmem = pgcov; + active = (shmem->nest_entrance[0] != '\0'); + if (active && entrance != NULL) + memcpy(entrance, pgcov->nest_entrance, MAX_ENTRANCE_SIZE); + LWLockRelease(pgcov->lock); + return active; +} + +/* +Datum pgcov_(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_); + +Datum +pgcov_(PG_FUNCTION_ARGS) +*/ + + +Datum pgcov_reset(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_reset); + +Datum +pgcov_reset(PG_FUNCTION_ARGS) +{ + if (pgcov_listener_mcxt == NULL) + { + /* nothing to do */ + PG_RETURN_VOID(); + } + + hash_destroy(pgcov_function_coverage); /* XXX any reason to do this? (any reason not to?) */ + pgcov_function_coverage = NULL; + + MemoryContextDelete(pgcov_listener_mcxt); + pgcov_listener_mcxt = NULL; + + PG_RETURN_VOID(); +} + + +Datum pgcov_listen(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_listen); + +Datum +pgcov_listen(PG_FUNCTION_ARGS) +{ + pgcovNest nest; + + /* + * See if there's already an active listener. There's a race between us + * checking for one and registering ourselves into the shared memory (since + * we don't hold the lock while creating the socket), but that's all right; + * pgcov_shmem_set_listener will raise an exception in that case. + */ + if (pgcov_get_active_listener(NULL)) + elog(ERROR, "an active listener already exists"); + + /* make sure we don't have any previous crap left */ + //DirectFunctionCall0(pgcov_reset); + pgcov_reset(NULL); // XXX WTF + + pgcov_init_listener(); + pgcov_start_listener(&nest); + PG_TRY(); + { + pgcov_shmem_set_listener(&nest); + /* now gather information until we're cancelled */ + pgcov_gather_information(&nest); + } + PG_CATCH(); + { + pgcov_shmem_clear_listener(&nest); + pgcov_stop_listener(&nest); + PG_RE_THROW(); + } + PG_END_TRY(); + + pgcov_shmem_clear_listener(&nest); + pgcov_stop_listener(&nest); + + PG_RETURN_NULL(); +} + +/* + * Enables the tester for pgcov_get_function_line_info(). + */ +Datum pgcov_enable_test_function_line_info(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(pgcov_enable_test_function_line_info); + +Datum +pgcov_enable_test_function_line_info(PG_FUNCTION_ARGS) +{ + pgcov_test_function_line_info = true; + return (Datum) 0; +} + +/* + * Fetches the function's signature. None of the types in pg_catalog are + * schema-qualified, other types always are. + * TODO actually implement that :-D + */ +static char * +get_function_signature(HeapTuple proctup, Form_pg_proc procform, const char *proname) +{ + StringInfoData str; + int nargs; + int i; + int input_argno; + Oid *argtypes; + char **argnames; + char *argmodes; + + initStringInfo(&str); + + appendStringInfo(&str, "%s(", proname); + nargs = get_func_arg_info(proctup, &argtypes, &argnames, &argmodes); + input_argno = 0; + for (i = 0; i < nargs; ++i) + { + Oid argtype = argtypes[i]; + + if (argmodes && + argmodes[i] != PROARGMODE_IN && + argmodes[i] != PROARGMODE_INOUT) + continue; + + if (input_argno++ > 0) + appendStringInfoString(&str, ", "); + + appendStringInfoString(&str, format_type_be(argtype)); + } + appendStringInfoChar(&str, ')'); + + return str.data; +} + +static bool +pgcov_get_function_line_info_enter_stmt(PLpgSQL_function *func, + PLpgSQL_stmt *stmt, + void *aux) +{ + Bitmapset **bms = (Bitmapset **) aux; + if (stmt->lineno > 0) + *bms = bms_add_member(*bms, stmt->lineno); + return false; +} + + +/* + * Finds all lines in a function which contain (the first line of) a PL/PgSQL + * statement. + * + * The caller must make sure we're in pgcov_call_stack_mctx. + */ +static void +pgcov_get_function_line_info(pgcovStackFrame *fn, + PLpgSQL_function *func, + const char *prosrc) +{ + fniter_context ctx; + Bitmapset *linebms = NULL; + + Assert(fn->lines == NIL); + + memset(&ctx, 0, sizeof(fniter_context)); + ctx.enter_stmt = pgcov_get_function_line_info_enter_stmt; + ctx.auxiliary = (void *) &linebms; + fniter_function_iterate(func, &ctx); + + if (linebms) + { + int lineno; + + while ((lineno = bms_first_member(linebms)) >= 0) + { + pgcovFunctionLine *line = + (pgcovFunctionLine *) palloc(sizeof(pgcovFunctionLine)); + line->lineno = (int32) lineno; + line->num_executed = 0; + fn->lines = lappend(fn->lines, line); + } + pfree(linebms); + } + + if (pgcov_test_function_line_info) + { + ListCell *lc; + pgcovFunctionLine *line; + + elog(INFO, "%d lines:", list_length(fn->lines)); + foreach(lc, fn->lines) + { + line = (pgcovFunctionLine *) lfirst(lc); + elog(INFO, " %d", line->lineno); + } + } +} + +static void +pgcov_plpgsql_func_beg(PLpgSQL_execstate *estate, + PLpgSQL_function *func) +{ + MemoryContext oldctx; + HeapTuple proctup; + Form_pg_proc procform; + pgcovStackFrame *fn; + Datum prosrc; + bool isnull; + + Assert(pgcov_call_stack != NIL); + + oldctx = MemoryContextSwitchTo(pgcov_call_stack_mctx); + + fn = (pgcovStackFrame *) linitial(pgcov_call_stack); + /* TODO: handle InvalidOid for DO blocks */ + if (fn->fnoid != func->fn_oid) + elog(ERROR, "PL/PgSQL function oid %u does not match stack frame %u", + func->fn_oid, fn->fnoid); + + /* look up prosrc */ + proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fn->fnoid)); + if (!HeapTupleIsValid(proctup)) + elog(ERROR, "cache lookup failed for function %u", fn->fnoid); + + procform = (Form_pg_proc) GETSTRUCT(proctup); + prosrc = SysCacheGetAttr(PROCOID, proctup, Anum_pg_proc_prosrc, &isnull); + if (isnull) + elog(ERROR, "unexpected null prosrc for function %u", fn->fnoid); + + fn->prosrc = TextDatumGetCString(prosrc); + ReleaseSysCache(proctup); + + /* .. and information about the statements in this function */ + pgcov_get_function_line_info(fn, func, fn->prosrc); + + pgcov_record_stmt_enter(estate, (PLpgSQL_stmt *) func->action); + + MemoryContextSwitchTo(oldctx); +} + +static void +pgcov_plpgsql_stmt_beg(PLpgSQL_execstate *estate, + PLpgSQL_stmt *stmt) +{ + Assert(pgcov_call_stack != NIL); + + /* skip dummy returns; see pl_comp.c */ + if (stmt->cmd_type == PLPGSQL_STMT_RETURN && + stmt->lineno == 0) + return; + + pgcov_record_stmt_enter(estate, stmt); +} + +static void +pgcov_record_stmt_enter(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt) +{ + pgcovStackFrame *fn; + ListCell *lc; + + Assert(pgcov_call_stack != NIL); + fn = (pgcovStackFrame *) linitial(pgcov_call_stack); + Assert(fn->fnoid == estate->func->fn_oid); + foreach(lc, fn->lines) + { + pgcovFunctionLine *line = (pgcovFunctionLine *) lfirst(lc); + if (line->lineno == (int32) stmt->lineno) + { + line->num_executed++; + return; + } + } + + /* XXX this probably shouldn't happen? */ + elog(WARNING, "could not find lineno %d of function %u, stmt %d", + stmt->lineno, fn->fnoid, stmt->cmd_type); +} + +static bool +pgcov_needs_fmgr_hook(Oid fnoid) +{ + /* always need */ + return true; +} + +static void +pgcov_enter_func_guts(Oid fnoid) +{ + MemoryContext oldctx; + pgcovStackFrame *newfn; + + oldctx = MemoryContextSwitchTo(pgcov_call_stack_mctx); + + newfn = (pgcovStackFrame *) palloc(sizeof(pgcovStackFrame)); + newfn->fnoid = fnoid; + newfn->lines = NIL; + + { + HeapTuple proctup; + Form_pg_proc procform; + const char *proname; + + proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fnoid)); + if (!HeapTupleIsValid(proctup)) + elog(ERROR, "cache lookup failed for function %d", fnoid); + + procform = (Form_pg_proc) GETSTRUCT(proctup); + proname = NameStr(procform->proname); + + newfn->fnsignature = get_function_signature(proctup, procform, proname); + + ReleaseSysCache(proctup); + } + /* + * If it's a PL/PgSQL function, pgcov_plpgsql_func_beg will fetch the + * actual prosrc for the function. + */ + newfn->prosrc = NULL; + pgcov_call_stack = lcons(newfn, pgcov_call_stack); + + MemoryContextSwitchTo(oldctx); +} + + +static void +pgcov_exit_func_guts(Oid fnoid) +{ + pgcovStackFrame *fn; + + Assert(pgcov_call_stack != NIL); + + /* if coverage reporting is enabled, send a coverage report now */ + fn = (pgcovStackFrame *) linitial(pgcov_call_stack); + if (fn->fnoid != fnoid) + elog(FATAL, "XXX %d != %d", fn->fnoid, fnoid); + + pgcov_emit_function_coverage_report(fn); + + pgcov_call_stack = list_delete_first(pgcov_call_stack); + if (pgcov_call_stack == NIL) + { + /* last frame, clean up */ + MemoryContextReset(pgcov_call_stack_mctx); + } +} + +static void +pgcov_fmgr_hook(FmgrHookEventType event, FmgrInfo *flinfo, Datum *args) +{ + switch (event) + { + case FHET_START: + pgcov_enter_func_guts(flinfo->fn_oid); + break; + + case FHET_END: + case FHET_ABORT: + // TODO + pgcov_exit_func_guts(flinfo->fn_oid); + break; + + default: + elog(ERROR, "unknown FmgrHookEventType %d", event); + } + + if (prev_fmgr_hook) + (*prev_fmgr_hook)(event, flinfo, args); +} + diff --git a/pgcov.control b/pgcov.control new file mode 100644 index 0000000..5e22639 --- /dev/null +++ b/pgcov.control @@ -0,0 +1,5 @@ +comment = 'pgcov' +default_version = '1.0' + +relocatable = false +schema = 'pgcov' diff --git a/pgcov.h b/pgcov.h new file mode 100644 index 0000000..7795d32 --- /dev/null +++ b/pgcov.h @@ -0,0 +1,153 @@ +#ifndef __PGCOV_MAIN_HEADER__ +#define __PGCOV_MAIN_HEADER__ + +#include "postgres.h" +#include "plpgsql.h" + +typedef struct { + int32 lineno; + int32 num_executed; +} pgcovFunctionLine; + +extern MemoryContext pgcov_listener_mcxt; + +typedef struct { + int32 depth; + Oid fnoid; + char *fnsignature; + + /* only populated if we're doing coverage reports */ + char *prosrc; + List *lines; +} pgcovStackFrame; + +/* func_iter.c */ +typedef struct fniter_context +{ + bool (*enter_stmt)(PLpgSQL_function *, PLpgSQL_stmt *, void *); + bool (*exit_stmt)(PLpgSQL_function *, PLpgSQL_stmt *, void *); + bool (*enter_body)(PLpgSQL_function *, PLpgSQL_stmt *, List *, void *); + bool (*exit_body)(PLpgSQL_function *, PLpgSQL_stmt *, List *, void *); + void *auxiliary; +} fniter_context; + +extern bool fniter_function_iterate(PLpgSQL_function *func, fniter_context *context); + +/* comm.c */ + +typedef enum { + PGCOV_MSG_COVERAGE_REPORT = 'C', + //PGCOV_MSG_STACK_FRAME = 'F', + + PGCOV_MSG_DONE = 'E' +} pgcovProtocolMessageType; + +typedef struct { + /* a connection is always only sending or receiving */ + union { + StringInfoData sendbuf; + StringInfoData rcvbuf; + }; + int sockfd; +} pgcovNetworkConn; + + +#define MAX_ENTRANCE_SIZE 16 + +typedef struct { + pgcovNetworkConn buf; + bool free; + bool done; +} pgcovWorker; + +typedef struct { + char entrance[MAX_ENTRANCE_SIZE]; + int lsockfd; + + int max_workers; + pgcovWorker *workers; + int nworkers; +} pgcovNest; + +extern void pgcov_function_coverage_sfunc(Oid fnoid, char *fnsignature, int32 ncalls, + char *prosrc, List *lines); + +extern bool pgcov_get_active_listener(char entrance[MAX_ENTRANCE_SIZE]); +extern void pgcov_start_listener(pgcovNest *nest); +extern void pgcov_stop_listener(pgcovNest *nest); +extern void pgcov_gather_information(pgcovNest *nest); +extern void pgcov_worker_connect(pgcovNetworkConn *conn, const char *congregation_area); + +extern void pgcov_emit_function_coverage_report(const pgcovStackFrame *fn); + +/* message parsing */ + +/* XXX Who wrote this crap? */ + +#define PGCOV_MESSAGE_PARSE_FUNC(fnname) \ + int fnname(const char *__pmsgdata, int32 __pmsglen, int32 __pmsgoff, pgcovNetworkConn *conn) + +#define PGCOV_PARSE(parsefn) \ + do { \ + int32 msglen; \ + MemoryContext oldcxt; \ + \ + *((uint32 *) &msglen) = nw_peek_uint32(conn->rcvbuf.data + 1); \ + \ + /* + * Switch to the parse context, and reset after we've parsed a message. + * This makes sure we never leak any memory in the parse functions. + * Note that any "sfuncs" will have to copy the data they get passed + * out of the parse context if they need it to live longer than the + * duration of the call. + */ \ + oldcxt = MemoryContextSwitchTo(pgcov_protocol_parse_mcxt); \ + parsefn(conn->rcvbuf.data + 5, msglen, 0, conn); \ + MemoryContextReset(pgcov_protocol_parse_mcxt); \ + nw_buf_discard_message(conn); \ + MemoryContextSwitchTo(oldcxt); \ + } while(0) + +#define _PGCOV_PARSE_NEED(c) \ + do { \ + if ((c) > __pmsglen - __pmsgoff) \ + elog(ERROR, "parse error: expected %d bytes, only %d bytes available", (c), __pmsglen - __pmsgoff); \ + } while(0) + +#define _PGCOV_PARSE_ADVANCE(c) do { __pmsgoff += (c); } while(0) + +#define PGCOV_PARSE_INT32(pint) \ + do { \ + _PGCOV_PARSE_NEED(4); \ + *((uint32 *) (pint)) = nw_peek_uint32(__pmsgdata + __pmsgoff); \ + _PGCOV_PARSE_ADVANCE(4); \ + } while(0) + +#define PGCOV_PARSE_UINT32(puint) \ + do { \ + _PGCOV_PARSE_NEED(4); \ + *(puint) = nw_peek_uint32(__pmsgdata + __pmsgoff); \ + _PGCOV_PARSE_ADVANCE(4); \ + } while(0) + + +#define PGCOV_PARSE_STRING(pstr) \ + do { \ + uint32 __pstrlen; \ + PGCOV_PARSE_INT32((int32 *) &__pstrlen); \ + if (__pstrlen == 0x80000001) \ + *(pstr) = NULL; \ + else { \ + _PGCOV_PARSE_NEED(__pstrlen + 1); \ + *(pstr) = palloc(__pstrlen + 1); \ + memcpy(*(pstr), \ + __pmsgdata + __pmsgoff, \ + __pstrlen + 1); \ + _PGCOV_PARSE_ADVANCE((int32) __pstrlen + 1);\ + } \ + } while(0) + +#define PGCOV_END_PARSE() \ + return __pmsgoff + +#endif