From 77ecfb9cdc92faea474cb91b65edd45c32a4ff40 Mon Sep 17 00:00:00 2001
From: Hannu Hanhi <hhanhipublic@gmail.com>
Date: Mon, 25 Oct 2021 20:49:15 +0300
Subject: [PATCH] Perfstats averaging and refactor

---
 src/d_clisrv.c             |   11 +-
 src/d_main.c               |   14 +-
 src/d_netcmd.c             |   12 +-
 src/d_netcmd.h             |    2 +
 src/hardware/hw_batching.c |   31 +-
 src/hardware/hw_main.c     |   70 +--
 src/hardware/hw_main.h     |   30 +-
 src/lua_hooklib.c          |    4 +-
 src/m_perfstats.c          | 1042 ++++++++++++++++++++++--------------
 src/m_perfstats.h          |   43 +-
 src/p_map.c                |    2 +-
 src/p_tick.c               |   20 +-
 src/r_bsp.c                |    4 +-
 src/r_main.c               |   49 +-
 src/r_main.h               |   32 +-
 src/r_things.c             |    2 +-
 src/z_zone.h               |    1 +
 17 files changed, 827 insertions(+), 542 deletions(-)

diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index b7071320c8..61b8553190 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -4974,16 +4974,23 @@ void TryRunTics(tic_t realtics)
 			// run the count * tics
 			while (neededtic > gametic)
 			{
+				boolean update_stats = !(paused || P_AutoPause());
+
 				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
 
-				ps_tictime = I_GetPreciseTime();
+				if (update_stats)
+					PS_START_TIMING(ps_tictime);
 
 				G_Ticker((gametic % NEWTICRATERATIO) == 0);
 				ExtraDataTicker();
 				gametic++;
 				consistancy[gametic%BACKUPTICS] = Consistancy();
 
-				ps_tictime = I_GetPreciseTime() - ps_tictime;
+				if (update_stats)
+				{
+					PS_STOP_TIMING(ps_tictime);
+					PS_UpdateTickStats();
+				}
 
 				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
 				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
diff --git a/src/d_main.c b/src/d_main.c
index b4b668f4bc..3845f08bd9 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -476,7 +476,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
-				ps_rendercalltime = I_GetPreciseTime();
+				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
 					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
@@ -523,7 +523,7 @@ static void D_Display(void)
 					if (postimgtype2)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
-				ps_rendercalltime = I_GetPreciseTime() - ps_rendercalltime;
+				PS_STOP_TIMING(ps_rendercalltime);
 			}
 
 			if (lastdraw)
@@ -537,7 +537,7 @@ static void D_Display(void)
 				lastdraw = false;
 			}
 
-			ps_uitime = I_GetPreciseTime();
+			PS_START_TIMING(ps_uitime);
 
 			if (gamestate == GS_LEVEL)
 			{
@@ -550,7 +550,7 @@ static void D_Display(void)
 		}
 		else
 		{
-			ps_uitime = I_GetPreciseTime();
+			PS_START_TIMING(ps_uitime);
 		}
 	}
 
@@ -592,7 +592,7 @@ static void D_Display(void)
 
 	CON_Drawer();
 
-	ps_uitime = I_GetPreciseTime() - ps_uitime;
+	PS_STOP_TIMING(ps_uitime);
 
 	//
 	// wipe update
@@ -678,9 +678,9 @@ static void D_Display(void)
 			M_DrawPerfStats();
 		}
 
-		ps_swaptime = I_GetPreciseTime();
+		PS_START_TIMING(ps_swaptime);
 		I_FinishUpdate(); // page flip or blit buffer
-		ps_swaptime = I_GetPreciseTime() - ps_swaptime;
+		PS_STOP_TIMING(ps_swaptime);
 	}
 }
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 5eb360befe..9d32f1bb1a 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -47,6 +47,7 @@
 #include "m_cond.h"
 #include "m_anigif.h"
 #include "md5.h"
+#include "m_perfstats.h"
 
 #ifdef NETGAME_DEVMODE
 #define CV_RESTRICT CV_NETVAR
@@ -374,7 +375,14 @@ consvar_t cv_sleep = CVAR_INIT ("cpusleep", "1", CV_SAVE, sleeping_cons_t, NULL)
 
 static CV_PossibleValue_t perfstats_cons_t[] = {
 	{0, "Off"}, {1, "Rendering"}, {2, "Logic"}, {3, "ThinkFrame"}, {0, NULL}};
-consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", 0, perfstats_cons_t, NULL);
+consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", CV_CALL, perfstats_cons_t, PS_PerfStats_OnChange);
+static CV_PossibleValue_t ps_samplesize_cons_t[] = {
+	{1, "MIN"}, {1000, "MAX"}, {0, NULL}};
+consvar_t cv_ps_samplesize = CVAR_INIT ("ps_samplesize", "1", CV_CALL, ps_samplesize_cons_t, PS_SampleSize_OnChange);
+static CV_PossibleValue_t ps_descriptor_cons_t[] = {
+	{1, "Average"}, {2, "SD"}, {3, "Minimum"}, {4, "Maximum"}, {0, NULL}};
+consvar_t cv_ps_descriptor = CVAR_INIT ("ps_descriptor", "Average", 0, ps_descriptor_cons_t, NULL);
+
 consvar_t cv_freedemocamera = CVAR_INIT("freedemocamera", "Off", CV_SAVE, CV_OnOff, NULL);
 
 char timedemo_name[256];
@@ -867,6 +875,8 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_soundtest);
 
 	CV_RegisterVar(&cv_perfstats);
+	CV_RegisterVar(&cv_ps_samplesize);
+	CV_RegisterVar(&cv_ps_descriptor);
 
 	// ingame object placing
 	COM_AddCommand("objectplace", Command_ObjectPlace_f);
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index cae32643e5..30b39c0eb7 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -110,6 +110,8 @@ extern consvar_t cv_skipmapcheck;
 extern consvar_t cv_sleep;
 
 extern consvar_t cv_perfstats;
+extern consvar_t cv_ps_samplesize;
+extern consvar_t cv_ps_descriptor;
 
 extern char timedemo_name[256];
 extern boolean timedemo_csv;
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index 0ac33d1361..da0319bccf 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -245,13 +245,16 @@ void HWR_RenderBatches(void)
 	currently_batching = false;// no longer collecting batches
 	if (!polygonArraySize)
 	{
-		ps_hw_numpolys = ps_hw_numcalls = ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 0;
+		ps_hw_numpolys.value.i = ps_hw_numcalls.value.i = ps_hw_numshaders.value.i
+			= ps_hw_numtextures.value.i = ps_hw_numpolyflags.value.i
+			= ps_hw_numcolors.value.i = 0;
 		return;// nothing to draw
 	}
 	// init stats vars
-	ps_hw_numpolys = polygonArraySize;
-	ps_hw_numcalls = ps_hw_numverts = 0;
-	ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 1;
+	ps_hw_numpolys.value.i = polygonArraySize;
+	ps_hw_numcalls.value.i = ps_hw_numverts.value.i = 0;
+	ps_hw_numshaders.value.i = ps_hw_numtextures.value.i
+		= ps_hw_numpolyflags.value.i = ps_hw_numcolors.value.i = 1;
 	// init polygonIndexArray
 	for (i = 0; i < polygonArraySize; i++)
 	{
@@ -259,12 +262,12 @@ void HWR_RenderBatches(void)
 	}
 
 	// sort polygons
-	ps_hw_batchsorttime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_batchsorttime);
 	if (cv_glshaders.value && gl_shadersavailable)
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygons);
 	else
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygonsNoShaders);
-	ps_hw_batchsorttime = I_GetPreciseTime() - ps_hw_batchsorttime;
+	PS_STOP_TIMING(ps_hw_batchsorttime);
 	// sort order
 	// 1. shader
 	// 2. texture
@@ -272,7 +275,7 @@ void HWR_RenderBatches(void)
 	// 4. colors + light level
 	// not sure about what order of the last 2 should be, or if it even matters
 
-	ps_hw_batchdrawtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_batchdrawtime);
 
 	currentShader = polygonArray[polygonIndexArray[0]].shader;
 	currentTexture = polygonArray[polygonIndexArray[0]].texture;
@@ -408,8 +411,8 @@ void HWR_RenderBatches(void)
 			// execute draw call
             HWD.pfnDrawIndexedTriangles(&currentSurfaceInfo, finalVertexArray, finalIndexWritePos, currentPolyFlags, finalVertexIndexArray);
 			// update stats
-			ps_hw_numcalls++;
-			ps_hw_numverts += finalIndexWritePos;
+			ps_hw_numcalls.value.i++;
+			ps_hw_numverts.value.i += finalIndexWritePos;
 			// reset write positions
 			finalVertexWritePos = 0;
 			finalIndexWritePos = 0;
@@ -426,7 +429,7 @@ void HWR_RenderBatches(void)
 			currentShader = nextShader;
 			changeShader = false;
 
-			ps_hw_numshaders++;
+			ps_hw_numshaders.value.i++;
 		}
 		if (changeTexture)
 		{
@@ -435,21 +438,21 @@ void HWR_RenderBatches(void)
 			currentTexture = nextTexture;
 			changeTexture = false;
 
-			ps_hw_numtextures++;
+			ps_hw_numtextures.value.i++;
 		}
 		if (changePolyFlags)
 		{
 			currentPolyFlags = nextPolyFlags;
 			changePolyFlags = false;
 
-			ps_hw_numpolyflags++;
+			ps_hw_numpolyflags.value.i++;
 		}
 		if (changeSurfaceInfo)
 		{
 			currentSurfaceInfo = nextSurfaceInfo;
 			changeSurfaceInfo = false;
 
-			ps_hw_numcolors++;
+			ps_hw_numcolors.value.i++;
 		}
 		// and that should be it?
 	}
@@ -457,7 +460,7 @@ void HWR_RenderBatches(void)
 	polygonArraySize = 0;
 	unsortedVertexArraySize = 0;
 
-	ps_hw_batchdrawtime = I_GetPreciseTime() - ps_hw_batchdrawtime;
+	PS_STOP_TIMING(ps_hw_batchdrawtime);
 }
 
 
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index e0851af851..9bade3d6fb 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -147,22 +147,22 @@ static angle_t gl_aimingangle;
 static void HWR_SetTransformAiming(FTransform *trans, player_t *player, boolean skybox);
 
 // Render stats
-precise_t ps_hw_skyboxtime = 0;
-precise_t ps_hw_nodesorttime = 0;
-precise_t ps_hw_nodedrawtime = 0;
-precise_t ps_hw_spritesorttime = 0;
-precise_t ps_hw_spritedrawtime = 0;
+ps_metric_t ps_hw_skyboxtime = {0};
+ps_metric_t ps_hw_nodesorttime = {0};
+ps_metric_t ps_hw_nodedrawtime = {0};
+ps_metric_t ps_hw_spritesorttime = {0};
+ps_metric_t ps_hw_spritedrawtime = {0};
 
 // Render stats for batching
-int ps_hw_numpolys = 0;
-int ps_hw_numverts = 0;
-int ps_hw_numcalls = 0;
-int ps_hw_numshaders = 0;
-int ps_hw_numtextures = 0;
-int ps_hw_numpolyflags = 0;
-int ps_hw_numcolors = 0;
-precise_t ps_hw_batchsorttime = 0;
-precise_t ps_hw_batchdrawtime = 0;
+ps_metric_t ps_hw_numpolys = {0};
+ps_metric_t ps_hw_numverts = {0};
+ps_metric_t ps_hw_numcalls = {0};
+ps_metric_t ps_hw_numshaders = {0};
+ps_metric_t ps_hw_numtextures = {0};
+ps_metric_t ps_hw_numpolyflags = {0};
+ps_metric_t ps_hw_numcolors = {0};
+ps_metric_t ps_hw_batchsorttime = {0};
+ps_metric_t ps_hw_batchdrawtime = {0};
 
 boolean gl_init = false;
 boolean gl_maploaded = false;
@@ -3235,7 +3235,7 @@ static void HWR_Subsector(size_t num)
 		}
 
 		// for render stats
-		ps_numpolyobjects += numpolys;
+		ps_numpolyobjects.value.i += numpolys;
 
 		// Sort polyobjects
 		R_SortPolyObjects(sub);
@@ -3343,7 +3343,7 @@ static void HWR_RenderBSPNode(INT32 bspnum)
 	// Decide which side the view point is on
 	INT32 side;
 
-	ps_numbspcalls++;
+	ps_numbspcalls.value.i++;
 
 	// Found a subsector?
 	if (bspnum & NF_SUBSECTOR)
@@ -4718,7 +4718,7 @@ static void HWR_CreateDrawNodes(void)
 	// that is already lying around. This should all be in some sort of linked list or lists.
 	sortindex = Z_Calloc(sizeof(size_t) * (numplanes + numpolyplanes + numwalls), PU_STATIC, NULL);
 
-	ps_hw_nodesorttime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_nodesorttime);
 
 	for (i = 0; i < numplanes; i++, p++)
 	{
@@ -4738,7 +4738,7 @@ static void HWR_CreateDrawNodes(void)
 		sortindex[p] = p;
 	}
 
-	ps_numdrawnodes = p;
+	ps_numdrawnodes.value.i = p;
 
 	// p is the number of stuff to sort
 
@@ -4773,9 +4773,9 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodesorttime = I_GetPreciseTime() - ps_hw_nodesorttime;
+	PS_STOP_TIMING(ps_hw_nodesorttime);
 
-	ps_hw_nodedrawtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_nodedrawtime);
 
 	// Okay! Let's draw it all! Woo!
 	HWD.pfnSetTransform(&atransform);
@@ -4812,7 +4812,7 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodedrawtime = I_GetPreciseTime() - ps_hw_nodedrawtime;
+	PS_STOP_TIMING(ps_hw_nodedrawtime);
 
 	numwalls = 0;
 	numplanes = 0;
@@ -6095,10 +6095,10 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	if (viewnumber == 0) // Only do it if it's the first screen being rendered
 		HWD.pfnClearBuffer(true, false, &ClearColor); // Clear the Color Buffer, stops HOMs. Also seems to fix the skybox issue on Intel GPUs.
 
-	ps_hw_skyboxtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_skyboxtime);
 	if (skybox && drawsky) // If there's a skybox and we should be drawing the sky, draw the skybox
 		HWR_RenderSkyboxView(viewnumber, player); // This is drawn before everything else so it is placed behind
-	ps_hw_skyboxtime = I_GetPreciseTime() - ps_hw_skyboxtime;
+	PS_STOP_TIMING(ps_hw_skyboxtime);
 
 	{
 		// do we really need to save player (is it not the same)?
@@ -6208,9 +6208,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	// Reset the shader state.
 	HWR_SetShaderState();
 
-	ps_numbspcalls = 0;
-	ps_numpolyobjects = 0;
-	ps_bsptime = I_GetPreciseTime();
+	ps_numbspcalls.value.i = 0;
+	ps_numpolyobjects.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 
 	validcount++;
 
@@ -6248,7 +6248,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	}
 #endif
 
-	ps_bsptime = I_GetPreciseTime() - ps_bsptime;
+	PS_STOP_TIMING(ps_bsptime);
 
 	if (cv_glbatching.value)
 		HWR_RenderBatches();
@@ -6263,22 +6263,22 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 #endif
 
 	// Draw MD2 and sprites
-	ps_numsprites = gl_visspritecount;
-	ps_hw_spritesorttime = I_GetPreciseTime();
+	ps_numsprites.value.i = gl_visspritecount;
+	PS_START_TIMING(ps_hw_spritesorttime);
 	HWR_SortVisSprites();
-	ps_hw_spritesorttime = I_GetPreciseTime() - ps_hw_spritesorttime;
-	ps_hw_spritedrawtime = I_GetPreciseTime();
+	PS_STOP_TIMING(ps_hw_spritesorttime);
+	PS_START_TIMING(ps_hw_spritedrawtime);
 	HWR_DrawSprites();
-	ps_hw_spritedrawtime = I_GetPreciseTime() - ps_hw_spritedrawtime;
+	PS_STOP_TIMING(ps_hw_spritedrawtime);
 
 #ifdef NEWCORONAS
 	//Hurdler: they must be drawn before translucent planes, what about gl fog?
 	HWR_DrawCoronas();
 #endif
 
-	ps_numdrawnodes = 0;
-	ps_hw_nodesorttime = 0;
-	ps_hw_nodedrawtime = 0;
+	ps_numdrawnodes.value.i = 0;
+	ps_hw_nodesorttime.value.p = 0;
+	ps_hw_nodedrawtime.value.p = 0;
 	if (numplanes || numpolyplanes || numwalls) //Hurdler: render 3D water and transparent walls after everything
 	{
 		HWR_CreateDrawNodes();
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index b751b2a6e1..3f90f0ae17 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -20,6 +20,8 @@
 #include "../d_player.h"
 #include "../r_defs.h"
 
+#include "../m_perfstats.h"
+
 // Startup & Shutdown the hardware mode renderer
 void HWR_Startup(void);
 void HWR_Switch(void);
@@ -116,22 +118,22 @@ extern FTransform atransform;
 
 
 // Render stats
-extern precise_t ps_hw_skyboxtime;
-extern precise_t ps_hw_nodesorttime;
-extern precise_t ps_hw_nodedrawtime;
-extern precise_t ps_hw_spritesorttime;
-extern precise_t ps_hw_spritedrawtime;
+extern ps_metric_t ps_hw_skyboxtime;
+extern ps_metric_t ps_hw_nodesorttime;
+extern ps_metric_t ps_hw_nodedrawtime;
+extern ps_metric_t ps_hw_spritesorttime;
+extern ps_metric_t ps_hw_spritedrawtime;
 
 // Render stats for batching
-extern int ps_hw_numpolys;
-extern int ps_hw_numverts;
-extern int ps_hw_numcalls;
-extern int ps_hw_numshaders;
-extern int ps_hw_numtextures;
-extern int ps_hw_numpolyflags;
-extern int ps_hw_numcolors;
-extern precise_t ps_hw_batchsorttime;
-extern precise_t ps_hw_batchdrawtime;
+extern ps_metric_t ps_hw_numpolys;
+extern ps_metric_t ps_hw_numverts;
+extern ps_metric_t ps_hw_numcalls;
+extern ps_metric_t ps_hw_numshaders;
+extern ps_metric_t ps_hw_numtextures;
+extern ps_metric_t ps_hw_numpolyflags;
+extern ps_metric_t ps_hw_numcolors;
+extern ps_metric_t ps_hw_batchsorttime;
+extern ps_metric_t ps_hw_batchdrawtime;
 
 extern boolean gl_init;
 extern boolean gl_maploaded;
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 32b5e52fb6..a72b22b5a6 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -505,7 +505,7 @@ static int call_hooks
 		calls += call_mobj_type_hooks(hook, MT_NULL);
 		calls += call_mobj_type_hooks(hook, hook->mobj_type);
 
-		ps_lua_mobjhooks += calls;
+		ps_lua_mobjhooks.value.i += calls;
 	}
 	else
 		calls += call_mapped(hook, &hookIds[hook->hook_type]);
@@ -868,7 +868,7 @@ void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 		LUA_PushUserdata(gL, line, META_LINE);
 		LUA_PushUserdata(gL, mo, META_MOBJ);
 		LUA_PushUserdata(gL, sector, META_SECTOR);
-		ps_lua_mobjhooks += call_hooks(&hook, 0, res_none);
+		ps_lua_mobjhooks.value.i += call_hooks(&hook, 0, res_none);
 	}
 }
 
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 8a99312e6a..9285b31be1 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -22,46 +22,173 @@
 #include "hardware/hw_main.h"
 #endif
 
-struct perfstatcol;
 struct perfstatrow;
 
-typedef struct perfstatcol perfstatcol_t;
 typedef struct perfstatrow perfstatrow_t;
 
-struct perfstatcol {
-	INT32 lores_x;
-	INT32 hires_x;
-	INT32 color;
-	perfstatrow_t * rows;
+struct perfstatrow {
+	const char  * lores_label;
+	const char  * hires_label;
+	ps_metric_t * metric;
+	UINT8         flags;
 };
 
-struct perfstatrow {
-	const char * lores_label;
-	const char * hires_label;
-	void       * value;
+// perfstatrow_t flags
+
+#define PS_TIME      1  // metric measures time (uses precise_t instead of INT32)
+#define PS_LEVEL     2  // metric is valid only when a level is active
+#define PS_SW        4  // metric is valid only in software mode
+#define PS_HW        8  // metric is valid only in opengl mode
+#define PS_BATCHING  16 // metric is valid only when opengl batching is active
+#define PS_HIDE_ZERO 32 // hide metric if its value is zero
+
+static ps_metric_t ps_frametime = {0};
+
+ps_metric_t ps_tictime = {0};
+
+ps_metric_t ps_playerthink_time = {0};
+ps_metric_t ps_thinkertime = {0};
+
+ps_metric_t ps_thlist_times[NUM_THINKERLISTS];
+
+static ps_metric_t ps_thinkercount = {0};
+static ps_metric_t ps_polythcount = {0};
+static ps_metric_t ps_mainthcount = {0};
+static ps_metric_t ps_mobjcount = {0};
+static ps_metric_t ps_regularcount = {0};
+static ps_metric_t ps_scenerycount = {0};
+static ps_metric_t ps_nothinkcount = {0};
+static ps_metric_t ps_dynslopethcount = {0};
+static ps_metric_t ps_precipcount = {0};
+static ps_metric_t ps_removecount = {0};
+
+ps_metric_t ps_checkposition_calls = {0};
+
+ps_metric_t ps_lua_thinkframe_time = {0};
+ps_metric_t ps_lua_mobjhooks = {0};
+
+ps_metric_t ps_otherlogictime = {0};
+
+// Columns for perfstats pages.
+
+// Position on screen is determined separately in the drawing functions.
+
+// New columns must also be added to the drawing and update functions.
+// Drawing functions: PS_DrawRenderStats, PS_DrawGameLogicStats, etc.
+// Update functions:
+//  - PS_UpdateFrameStats for frame-dependent values
+//  - PS_UpdateTickStats for tick-dependent values
+
+// Rendering stats columns
+
+perfstatrow_t rendertime_rows[] = {
+	{"frmtime", "Frame time:    ", &ps_frametime, PS_TIME},
+	{"drwtime", "3d rendering:  ", &ps_rendercalltime, PS_TIME|PS_LEVEL},
+
+#ifdef HWRENDER
+	{" skybox ", " Skybox render: ", &ps_hw_skyboxtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_HW},
+	{" batsort", " Batch sort:    ", &ps_hw_batchsorttime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
+	{" batdraw", " Batch render:  ", &ps_hw_batchdrawtime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
+	{" sprsort", " Sprite sort:   ", &ps_hw_spritesorttime, PS_TIME|PS_LEVEL|PS_HW},
+	{" sprdraw", " Sprite render: ", &ps_hw_spritedrawtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" nodesrt", " Drwnode sort:  ", &ps_hw_nodesorttime, PS_TIME|PS_LEVEL|PS_HW},
+	{" nodedrw", " Drwnode render:", &ps_hw_nodedrawtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_HW},
+#endif
+
+	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_SW},
+	{" sprclip", " R_ClipSprites: ", &ps_sw_spritecliptime, PS_TIME|PS_LEVEL|PS_SW},
+	{" portals", " Portals+Skybox:", &ps_sw_portaltime, PS_TIME|PS_LEVEL|PS_SW},
+	{" planes ", " R_DrawPlanes:  ", &ps_sw_planetime, PS_TIME|PS_LEVEL|PS_SW},
+	{" masked ", " R_DrawMasked:  ", &ps_sw_maskedtime, PS_TIME|PS_LEVEL|PS_SW},
+	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_SW},
+
+	{"ui     ", "UI render:     ", &ps_uitime, PS_TIME},
+	{"finupdt", "I_FinishUpdate:", &ps_swaptime, PS_TIME},
+	{0}
 };
 
-static precise_t ps_frametime = 0;
+perfstatrow_t gamelogicbrief_row[] = {
+	{"logic  ", "Game logic:    ", &ps_tictime, PS_TIME},
+	{0}
+};
 
-precise_t ps_tictime = 0;
+perfstatrow_t commoncounter_rows[] = {
+	{"bspcall", "BSP calls:   ", &ps_numbspcalls, 0},
+	{"sprites", "Sprites:     ", &ps_numsprites, 0},
+	{"drwnode", "Drawnodes:   ", &ps_numdrawnodes, 0},
+	{"plyobjs", "Polyobjects: ", &ps_numpolyobjects, 0},
+	{0}
+};
 
-precise_t ps_playerthink_time = 0;
-precise_t ps_thinkertime = 0;
+#ifdef HWRENDER
+perfstatrow_t batchcount_rows[] = {
+	{"polygon", "Polygons:  ", &ps_hw_numpolys, 0},
+	{"vertex ", "Vertices:  ", &ps_hw_numverts, 0},
+	{0}
+};
 
-precise_t ps_thlist_times[NUM_THINKERLISTS];
+perfstatrow_t batchcalls_rows[] = {
+	{"drwcall", "Draw calls:", &ps_hw_numcalls, 0},
+	{"shaders", "Shaders:   ", &ps_hw_numshaders, 0},
+	{"texture", "Textures:  ", &ps_hw_numtextures, 0},
+	{"polyflg", "Polyflags: ", &ps_hw_numpolyflags, 0},
+	{"colors ", "Colors:    ", &ps_hw_numcolors, 0},
+	{0}
+};
+#endif
 
-int ps_checkposition_calls = 0;
+// Game logic stats columns
+
+perfstatrow_t gamelogic_rows[] = {
+	{"logic  ", "Game logic:     ", &ps_tictime, PS_TIME},
+	{" plrthnk", " P_PlayerThink:  ", &ps_playerthink_time, PS_TIME|PS_LEVEL},
+	{" thnkers", " P_RunThinkers:  ", &ps_thinkertime, PS_TIME|PS_LEVEL},
+	{"  plyobjs", "  Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ], PS_TIME|PS_LEVEL},
+	{"  main   ", "  Main:           ", &ps_thlist_times[THINK_MAIN], PS_TIME|PS_LEVEL},
+	{"  mobjs  ", "  Mobjs:          ", &ps_thlist_times[THINK_MOBJ], PS_TIME|PS_LEVEL},
+	{"  dynslop", "  Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE], PS_TIME|PS_LEVEL},
+	{"  precip ", "  Precipitation:  ", &ps_thlist_times[THINK_PRECIP], PS_TIME|PS_LEVEL},
+	{" lthinkf", " LUAh_ThinkFrame:", &ps_lua_thinkframe_time, PS_TIME|PS_LEVEL},
+	{" other  ", " Other:          ", &ps_otherlogictime, PS_TIME|PS_LEVEL},
+	{0}
+};
 
-precise_t ps_lua_thinkframe_time = 0;
-int ps_lua_mobjhooks = 0;
+perfstatrow_t thinkercount_rows[] = {
+	{"thnkers", "Thinkers:       ", &ps_thinkercount, PS_LEVEL},
+	{" plyobjs", " Polyobjects:    ", &ps_polythcount, PS_LEVEL},
+	{" main   ", " Main:           ", &ps_mainthcount, PS_LEVEL},
+	{" mobjs  ", " Mobjs:          ", &ps_mobjcount, PS_LEVEL},
+	{"  regular", "  Regular:        ", &ps_regularcount, PS_LEVEL},
+	{"  scenery", "  Scenery:        ", &ps_scenerycount, PS_LEVEL},
+	{"  nothink", "  Nothink:        ", &ps_nothinkcount, PS_HIDE_ZERO|PS_LEVEL},
+	{" dynslop", " Dynamic slopes: ", &ps_dynslopethcount, PS_LEVEL},
+	{" precip ", " Precipitation:  ", &ps_precipcount, PS_LEVEL},
+	{" remove ", " Pending removal:", &ps_removecount, PS_LEVEL},
+	{0}
+};
+
+perfstatrow_t misc_calls_rows[] = {
+	{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks, PS_LEVEL},
+	{"chkpos", "P_CheckPosition:", &ps_checkposition_calls, PS_LEVEL},
+	{0}
+};
+
+// Sample collection status for averaging.
+// Maximum of these two is shown to user if nonzero to tell that
+// the reported averages are not correct yet.
+int ps_frame_samples_left = 0;
+int ps_tick_samples_left = 0;
+// History writing positions for frame and tick based metrics
+int ps_frame_index = 0;
+int ps_tick_index = 0;
 
 // dynamically allocated resizeable array for thinkframe hook stats
 ps_hookinfo_t *thinkframe_hooks = NULL;
 int thinkframe_hooks_length = 0;
 int thinkframe_hooks_capacity = 16;
 
-static INT32 draw_row;
-
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
 {
 	if (!thinkframe_hooks)
@@ -76,506 +203,617 @@ void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
 		thinkframe_hooks = Z_Realloc(thinkframe_hooks,
 			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
 	}
-	thinkframe_hooks[index].time_taken = time_taken;
+	thinkframe_hooks[index].time_taken.value.p = time_taken;
 	memcpy(thinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
 	// since the values are set sequentially from begin to end, the last call should leave
 	// the correct value to this variable
 	thinkframe_hooks_length = index + 1;
 }
 
-static void PS_SetFrameTime(void)
+static boolean PS_HighResolution(void)
 {
-	precise_t currenttime = I_GetPreciseTime();
-	ps_frametime = currenttime - ps_prevframetime;
-	ps_prevframetime = currenttime;
+	return (vid.width >= 640 && vid.height >= 400);
 }
 
-static boolean M_HighResolution(void)
+static boolean PS_IsLevelActive(void)
 {
-	return (vid.width >= 640 && vid.height >= 400);
+	return gamestate == GS_LEVEL ||
+			(gamestate == GS_TITLESCREEN && titlemapinaction);
 }
 
-enum {
-	PERF_TIME,
-	PERF_COUNT,
-};
+// Is the row valid in the current context?
+static boolean PS_IsRowValid(perfstatrow_t *row)
+{
+	return !((row->flags & PS_LEVEL && !PS_IsLevelActive()) ||
+		(row->flags & PS_SW && rendermode != render_soft) ||
+		(row->flags & PS_HW && rendermode != render_opengl) ||
+		(row->flags & PS_BATCHING && !cv_glbatching.value));
+}
 
-static void M_DrawPerfString(perfstatcol_t *col, int type)
+// Should the row be visible on the screen?
+static boolean PS_IsRowVisible(perfstatrow_t *row)
 {
-	const boolean hires = M_HighResolution();
+	boolean value_is_zero;
 
-	INT32 draw_flags = V_MONOSPACE | col->color;
+	if (row->flags & PS_TIME)
+		value_is_zero = row->metric->value.p == 0;
+	else
+		value_is_zero = row->metric->value.i == 0;
 
-	perfstatrow_t * row;
+	return !(!PS_IsRowValid(row) ||
+		(row->flags & PS_HIDE_ZERO && value_is_zero));
+}
+
+static INT32 PS_GetMetricAverage(ps_metric_t *metric, boolean time_metric)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT64 sum = 0;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		if (time_metric)
+			sum += I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			sum += *((INT32*)history_read_pos);
+		history_read_pos += value_size;
+	}
+
+	return sum / cv_ps_samplesize.value;
+}
+
+static INT32 PS_GetMetricMinOrMax(ps_metric_t *metric, boolean time_metric, boolean get_max)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT32 found_value = get_max ? INT32_MIN : INT32_MAX;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		INT32 value;
+		if (time_metric)
+			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			value = *((INT32*)history_read_pos);
+
+		if ((get_max && value > found_value) ||
+			(!get_max && value < found_value))
+		{
+			found_value = value;
+		}
+		history_read_pos += value_size;
+	}
+
+	return found_value;
+}
+
+// Calculates the standard deviation for metric.
+static INT32 PS_GetMetricSD(ps_metric_t *metric, boolean time_metric)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT64 sum = 0;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+	INT32 avg = PS_GetMetricAverage(metric, time_metric);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		INT64 value;
+		if (time_metric)
+			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			value = *((INT32*)history_read_pos);
+
+		value -= avg;
+		sum += value * value;
+
+		history_read_pos += value_size;
+	}
+
+	return round(sqrt(sum / cv_ps_samplesize.value));
+}
 
-	int value;
+// Returns the value to show on screen for metric.
+static INT32 PS_GetMetricScreenValue(ps_metric_t *metric, boolean time_metric)
+{
+	if (cv_ps_samplesize.value > 1 && metric->history)
+	{
+		if (cv_ps_descriptor.value == 1)
+			return PS_GetMetricAverage(metric, time_metric);
+		else if (cv_ps_descriptor.value == 2)
+			return PS_GetMetricSD(metric, time_metric);
+		else if (cv_ps_descriptor.value == 3)
+			return PS_GetMetricMinOrMax(metric, time_metric, false);
+		else
+			return PS_GetMetricMinOrMax(metric, time_metric, true);
+	}
+	else
+	{
+		if (time_metric)
+			return I_PreciseToMicros(metric->value.p);
+		else
+			return metric->value.i;
+	}
+}
+
+static int PS_DrawPerfRows(int x, int y, int color, perfstatrow_t *rows)
+{
+	const boolean hires = PS_HighResolution();
+	INT32 draw_flags = V_MONOSPACE | color;
+	perfstatrow_t * row;
+	int draw_y = y;
 
 	if (hires)
 		draw_flags |= V_ALLOWLOWERCASE;
 
-	for (row = col->rows; row->lores_label; ++row)
+	for (row = rows; row->lores_label; ++row)
 	{
-		if (type == PERF_TIME)
-			value = I_PreciseToMicros(*(precise_t *)row->value);
-		else
-			value = *(int *)row->value;
+		const char *label;
+		INT32 value;
+		char *final_str;
+
+		if (!PS_IsRowVisible(row))
+			continue;
+
+		label = hires ? row->hires_label : row->lores_label;
+		value = PS_GetMetricScreenValue(row->metric, !!(row->flags & PS_TIME));
+		final_str = va("%s %d", label, value);
 
 		if (hires)
 		{
-			V_DrawSmallString(col->hires_x, draw_row, draw_flags,
-					va("%s %d", row->hires_label, value));
-
-			draw_row += 5;
+			V_DrawSmallString(x, draw_y, draw_flags, final_str);
+			draw_y += 5;
 		}
 		else
 		{
-			V_DrawThinString(col->lores_x, draw_row, draw_flags,
-					va("%s %d", row->lores_label, value));
-
-			draw_row += 8;
+			V_DrawThinString(x, draw_y, draw_flags, final_str);
+			draw_y += 8;
 		}
 	}
+
+	return draw_y;
 }
 
-static void M_DrawPerfTiming(perfstatcol_t *col)
+static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric, boolean set_user)
 {
-	M_DrawPerfString(col, PERF_TIME);
+	int index = frame_metric ? ps_frame_index : ps_tick_index;
+
+	if (!metric->history)
+	{
+		// allocate history table
+		int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+		void** memory_user = set_user ? &metric->history : NULL;
+
+		metric->history = Z_Calloc(value_size * cv_ps_samplesize.value, PU_PERFSTATS,
+				memory_user);
+
+		// reset "samples left" counter since this history table needs to be filled
+		if (frame_metric)
+			ps_frame_samples_left = cv_ps_samplesize.value;
+		else
+			ps_tick_samples_left = cv_ps_samplesize.value;
+	}
+
+	if (time_metric)
+	{
+		precise_t *history = (precise_t*)metric->history;
+		history[index] = metric->value.p;
+	}
+	else
+	{
+		INT32 *history = (INT32*)metric->history;
+		history[index] = metric->value.i;
+	}
 }
 
-static void M_DrawPerfCount(perfstatcol_t *col)
+static void PS_UpdateRowHistories(perfstatrow_t *rows, boolean frame_metric)
 {
-	M_DrawPerfString(col, PERF_COUNT);
+	perfstatrow_t *row;
+	for (row = rows; row->lores_label; row++)
+	{
+		if (PS_IsRowValid(row))
+			PS_UpdateMetricHistory(row->metric, !!(row->flags & PS_TIME), frame_metric, true);
+	}
 }
 
-static void M_DrawRenderStats(void)
+// Update all metrics that are calculated on every frame.
+static void PS_UpdateFrameStats(void)
 {
-	const boolean hires = M_HighResolution();
-
-	const int half_row = hires ? 5 : 4;
+	// update frame time
+	precise_t currenttime = I_GetPreciseTime();
+	ps_frametime.value.p = currenttime - ps_prevframetime;
+	ps_prevframetime = currenttime;
 
-	precise_t extrarendertime;
-
-	perfstatrow_t frametime_row[] = {
-		{"frmtime", "Frame time:    ", &ps_frametime},
-		{0}
-	};
-
-	perfstatrow_t rendercalltime_row[] = {
-		{"drwtime", "3d rendering:  ", &ps_rendercalltime},
-		{0}
-	};
-
-	perfstatrow_t opengltime_row[] = {
-		{"skybox ", "Skybox render: ", &ps_hw_skyboxtime},
-		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
-		{"nodesrt", "Drwnode sort:  ", &ps_hw_nodesorttime},
-		{"nodedrw", "Drwnode render:", &ps_hw_nodedrawtime},
-		{"sprsort", "Sprite sort:   ", &ps_hw_spritesorttime},
-		{"sprdraw", "Sprite render: ", &ps_hw_spritedrawtime},
-		{"other  ", "Other:         ", &extrarendertime},
-		{0}
-	};
-
-	perfstatrow_t softwaretime_row[] = {
-		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
-		{"sprclip", "R_ClipSprites: ", &ps_sw_spritecliptime},
-		{"portals", "Portals+Skybox:", &ps_sw_portaltime},
-		{"planes ", "R_DrawPlanes:  ", &ps_sw_planetime},
-		{"masked ", "R_DrawMasked:  ", &ps_sw_maskedtime},
-		{"other  ", "Other:         ", &extrarendertime},
-		{0}
-	};
-
-	perfstatrow_t uiswaptime_row[] = {
-		{"ui     ", "UI render:     ", &ps_uitime},
-		{"finupdt", "I_FinishUpdate:", &ps_swaptime},
-		{0}
-	};
-
-	perfstatrow_t tictime_row[] = {
-		{"logic  ", "Game logic:    ", &ps_tictime},
-		{0}
-	};
-
-	perfstatrow_t rendercalls_row[] = {
-		{"bspcall", "BSP calls:   ", &ps_numbspcalls},
-		{"sprites", "Sprites:     ", &ps_numsprites},
-		{"drwnode", "Drawnodes:   ", &ps_numdrawnodes},
-		{"plyobjs", "Polyobjects: ", &ps_numpolyobjects},
-		{0}
-	};
-
-	perfstatrow_t batchtime_row[] = {
-		{"batsort", "Batch sort:  ", &ps_hw_batchsorttime},
-		{"batdraw", "Batch render:", &ps_hw_batchdrawtime},
-		{0}
-	};
-
-	perfstatrow_t batchcount_row[] = {
-		{"polygon", "Polygons:  ", &ps_hw_numpolys},
-		{"vertex ", "Vertices:  ", &ps_hw_numverts},
-		{0}
-	};
-
-	perfstatrow_t batchcalls_row[] = {
-		{"drwcall", "Draw calls:", &ps_hw_numcalls},
-		{"shaders", "Shaders:   ", &ps_hw_numshaders},
-		{"texture", "Textures:  ", &ps_hw_numtextures},
-		{"polyflg", "Polyflags: ", &ps_hw_numpolyflags},
-		{"colors ", "Colors:    ", &ps_hw_numcolors},
-		{0}
-	};
-
-	perfstatcol_t      frametime_col =  {20,  20, V_YELLOWMAP,      frametime_row};
-	perfstatcol_t rendercalltime_col =  {20,  20, V_YELLOWMAP, rendercalltime_row};
-
-	perfstatcol_t     opengltime_col =  {24,  24, V_YELLOWMAP,     opengltime_row};
-	perfstatcol_t   softwaretime_col =  {24,  24, V_YELLOWMAP,   softwaretime_row};
-
-	perfstatcol_t     uiswaptime_col =  {20,  20, V_YELLOWMAP,     uiswaptime_row};
-	perfstatcol_t        tictime_col =  {20,  20, V_GRAYMAP,          tictime_row};
-
-	perfstatcol_t    rendercalls_col =  {90, 115, V_BLUEMAP,      rendercalls_row};
-
-	perfstatcol_t      batchtime_col =  {90, 115, V_REDMAP,         batchtime_row};
-
-	perfstatcol_t     batchcount_col = {155, 200, V_PURPLEMAP,     batchcount_row};
-	perfstatcol_t     batchcalls_col = {220, 200, V_PURPLEMAP,     batchcalls_row};
-
-
-	boolean rendering = (
-			gamestate == GS_LEVEL ||
-			(gamestate == GS_TITLESCREEN && titlemapinaction)
-	);
-
-	draw_row = 10;
-	M_DrawPerfTiming(&frametime_col);
-
-	if (rendering)
+	// update 3d rendering stats
+	if (PS_IsLevelActive())
 	{
-		M_DrawPerfTiming(&rendercalltime_col);
-
 		// Remember to update this calculation when adding more 3d rendering stats!
-		extrarendertime = ps_rendercalltime - ps_bsptime;
+		ps_otherrendertime.value.p = ps_rendercalltime.value.p - ps_bsptime.value.p;
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl)
 		{
-			extrarendertime -=
-				ps_hw_skyboxtime +
-				ps_hw_nodesorttime +
-				ps_hw_nodedrawtime +
-				ps_hw_spritesorttime +
-				ps_hw_spritedrawtime;
+			ps_otherrendertime.value.p -=
+				ps_hw_skyboxtime.value.p +
+				ps_hw_nodesorttime.value.p +
+				ps_hw_nodedrawtime.value.p +
+				ps_hw_spritesorttime.value.p +
+				ps_hw_spritedrawtime.value.p;
 
 			if (cv_glbatching.value)
 			{
-				extrarendertime -=
-					ps_hw_batchsorttime +
-					ps_hw_batchdrawtime;
+				ps_otherrendertime.value.p -=
+					ps_hw_batchsorttime.value.p +
+					ps_hw_batchdrawtime.value.p;
 			}
-
-			M_DrawPerfTiming(&opengltime_col);
 		}
 		else
 #endif
 		{
-			extrarendertime -=
-				ps_sw_spritecliptime +
-				ps_sw_portaltime +
-				ps_sw_planetime +
-				ps_sw_maskedtime;
-
-			M_DrawPerfTiming(&softwaretime_col);
+			ps_otherrendertime.value.p -=
+				ps_sw_spritecliptime.value.p +
+				ps_sw_portaltime.value.p +
+				ps_sw_planetime.value.p +
+				ps_sw_maskedtime.value.p;
 		}
 	}
 
-	M_DrawPerfTiming(&uiswaptime_col);
-
-	draw_row += half_row;
-	M_DrawPerfTiming(&tictime_col);
-
-	if (rendering)
+	if (cv_ps_samplesize.value > 1)
 	{
-		draw_row = 10;
-		M_DrawPerfCount(&rendercalls_col);
+		PS_UpdateRowHistories(rendertime_rows, true);
+		if (PS_IsLevelActive())
+			PS_UpdateRowHistories(commoncounter_rows, true);
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl && cv_glbatching.value)
 		{
-			draw_row += half_row;
-			M_DrawPerfTiming(&batchtime_col);
-
-			draw_row = 10;
-			M_DrawPerfCount(&batchcount_col);
-
-			if (hires)
-				draw_row += half_row;
-			else
-				draw_row  = 10;
-
-			M_DrawPerfCount(&batchcalls_col);
+			PS_UpdateRowHistories(batchcount_rows, true);
+			PS_UpdateRowHistories(batchcalls_rows, true);
 		}
 #endif
+
+		ps_frame_index++;
+		if (ps_frame_index >= cv_ps_samplesize.value)
+			ps_frame_index = 0;
+		if (ps_frame_samples_left)
+			ps_frame_samples_left--;
 	}
 }
 
-static void M_DrawTickStats(void)
+// Update thinker counters by iterating the thinker lists.
+static void PS_CountThinkers(void)
 {
-	int i = 0;
+	int i;
 	thinker_t *thinker;
-	int thinkercount = 0;
-	int polythcount = 0;
-	int mainthcount = 0;
-	int mobjcount = 0;
-	int nothinkcount = 0;
-	int scenerycount = 0;
-	int regularcount = 0;
-	int dynslopethcount = 0;
-	int precipcount = 0;
-	int removecount = 0;
-
-	precise_t extratime =
-		ps_tictime -
-		ps_playerthink_time -
-		ps_thinkertime -
-		ps_lua_thinkframe_time;
-
-	perfstatrow_t tictime_row[] = {
-		{"logic  ", "Game logic:     ", &ps_tictime},
-		{0}
-	};
-
-	perfstatrow_t thinker_time_row[] = {
-		{"plrthnk", "P_PlayerThink:  ", &ps_playerthink_time},
-		{"thnkers", "P_RunThinkers:  ", &ps_thinkertime},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinker_time_row[] = {
-		{"plyobjs", "Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ]},
-		{"main   ", "Main:           ", &ps_thlist_times[THINK_MAIN]},
-		{"mobjs  ", "Mobjs:          ", &ps_thlist_times[THINK_MOBJ]},
-		{"dynslop", "Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE]},
-		{"precip ", "Precipitation:  ", &ps_thlist_times[THINK_PRECIP]},
-		{0}
-	};
-
-	perfstatrow_t extra_thinker_time_row[] = {
-		{"lthinkf", "LUAh_ThinkFrame:", &ps_lua_thinkframe_time},
-		{"other  ", "Other:          ", &extratime},
-		{0}
-	};
-
-	perfstatrow_t thinkercount_row[] = {
-		{"thnkers", "Thinkers:       ", &thinkercount},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinkercount_row[] = {
-		{"plyobjs", "Polyobjects:    ", &polythcount},
-		{"main   ", "Main:           ", &mainthcount},
-		{"mobjs  ", "Mobjs:          ", &mobjcount},
-		{0}
-	};
-
-	perfstatrow_t mobjthinkercount_row[] = {
-		{"regular", "Regular:        ", &regularcount},
-		{"scenery", "Scenery:        ", &scenerycount},
-		{0}
-	};
-
-	perfstatrow_t nothinkcount_row[] = {
-		{"nothink", "Nothink:        ", &nothinkcount},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinkercount_row2[] = {
-		{"dynslop", "Dynamic slopes: ", &dynslopethcount},
-		{"precip ", "Precipitation:  ", &precipcount},
-		{"remove ", "Pending removal:", &removecount},
-		{0}
-	};
-
-	perfstatrow_t misc_calls_row[] = {
-		{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks},
-		{"chkpos", "P_CheckPosition:", &ps_checkposition_calls},
-		{0}
-	};
-
-	perfstatcol_t               tictime_col  =  {20,  20, V_YELLOWMAP,               tictime_row};
-	perfstatcol_t          thinker_time_col  =  {24,  24, V_YELLOWMAP,          thinker_time_row};
-	perfstatcol_t detailed_thinker_time_col  =  {28,  28, V_YELLOWMAP, detailed_thinker_time_row};
-	perfstatcol_t    extra_thinker_time_col  =  {24,  24, V_YELLOWMAP,    extra_thinker_time_row};
-
-	perfstatcol_t          thinkercount_col  =  {90, 115, V_BLUEMAP,            thinkercount_row};
-	perfstatcol_t detailed_thinkercount_col  =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row};
-	perfstatcol_t      mobjthinkercount_col  =  {98, 123, V_BLUEMAP,        mobjthinkercount_row};
-	perfstatcol_t          nothinkcount_col  =  {98, 123, V_BLUEMAP,            nothinkcount_row};
-	perfstatcol_t detailed_thinkercount_col2 =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row2};
-	perfstatcol_t            misc_calls_col  = {170, 216, V_PURPLEMAP,            misc_calls_row};
+
+	ps_thinkercount.value.i = 0;
+	ps_polythcount.value.i = 0;
+	ps_mainthcount.value.i = 0;
+	ps_mobjcount.value.i = 0;
+	ps_regularcount.value.i = 0;
+	ps_scenerycount.value.i = 0;
+	ps_nothinkcount.value.i = 0;
+	ps_dynslopethcount.value.i = 0;
+	ps_precipcount.value.i = 0;
+	ps_removecount.value.i = 0;
 
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
 		for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
 		{
-			thinkercount++;
+			ps_thinkercount.value.i++;
 			if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-				removecount++;
+				ps_removecount.value.i++;
 			else if (i == THINK_POLYOBJ)
-				polythcount++;
+				ps_polythcount.value.i++;
 			else if (i == THINK_MAIN)
-				mainthcount++;
+				ps_mainthcount.value.i++;
 			else if (i == THINK_MOBJ)
 			{
 				if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
 				{
 					mobj_t *mobj = (mobj_t*)thinker;
-					mobjcount++;
+					ps_mobjcount.value.i++;
 					if (mobj->flags & MF_NOTHINK)
-						nothinkcount++;
+						ps_nothinkcount.value.i++;
 					else if (mobj->flags & MF_SCENERY)
-						scenerycount++;
+						ps_scenerycount.value.i++;
 					else
-						regularcount++;
+						ps_regularcount.value.i++;
 				}
 			}
 			else if (i == THINK_DYNSLOPE)
-				dynslopethcount++;
+				ps_dynslopethcount.value.i++;
 			else if (i == THINK_PRECIP)
-				precipcount++;
+				ps_precipcount.value.i++;
 		}
 	}
+}
 
-	draw_row = 10;
-	M_DrawPerfTiming(&tictime_col);
-	M_DrawPerfTiming(&thinker_time_col);
-	M_DrawPerfTiming(&detailed_thinker_time_col);
-	M_DrawPerfTiming(&extra_thinker_time_col);
-
-	draw_row = 10;
-	M_DrawPerfCount(&thinkercount_col);
-	M_DrawPerfCount(&detailed_thinkercount_col);
-	M_DrawPerfCount(&mobjthinkercount_col);
+// Update all metrics that are calculated on every tick.
+void PS_UpdateTickStats(void)
+{
+	if (cv_perfstats.value == 1 && cv_ps_samplesize.value > 1)
+	{
+		PS_UpdateRowHistories(gamelogicbrief_row, false);
+	}
+	if (cv_perfstats.value == 2)
+	{
+		if (PS_IsLevelActive())
+		{
+			ps_otherlogictime.value.p =
+				ps_tictime.value.p -
+				ps_playerthink_time.value.p -
+				ps_thinkertime.value.p -
+				ps_lua_thinkframe_time.value.p;
 
-	if (nothinkcount)
-		M_DrawPerfCount(&nothinkcount_col);
+			PS_CountThinkers();
+		}
 
-	M_DrawPerfCount(&detailed_thinkercount_col2);
+		if (cv_ps_samplesize.value > 1)
+		{
+			PS_UpdateRowHistories(gamelogic_rows, false);
+			PS_UpdateRowHistories(thinkercount_rows, false);
+			PS_UpdateRowHistories(misc_calls_rows, false);
+		}
+	}
+	if (cv_perfstats.value == 3 && cv_ps_samplesize.value > 1 && PS_IsLevelActive())
+	{
+		int i;
+		for (i = 0; i < thinkframe_hooks_length; i++)
+		{
+			PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false, false);
+		}
+	}
+	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
+	{
+		ps_tick_index++;
+		if (ps_tick_index >= cv_ps_samplesize.value)
+			ps_tick_index = 0;
+		if (ps_tick_samples_left)
+			ps_tick_samples_left--;
+	}
+}
 
-	if (M_HighResolution())
+static void PS_DrawDescriptorHeader(void)
+{
+	if (cv_ps_samplesize.value > 1)
 	{
-		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");
+		const char* descriptor_names[] = {
+			"average",
+			"standard deviation",
+			"minimum",
+			"maximum"
+		};
+		const boolean hires = PS_HighResolution();
+		char* str;
+		INT32 flags = V_MONOSPACE | V_ALLOWLOWERCASE;
+		int samples_left = max(ps_frame_samples_left, ps_tick_samples_left);
+		int x, y;
+
+		if (cv_perfstats.value == 3)
+		{
+			x = 2;
+			y = 0;
+		}
+		else
+		{
+			x = 20;
+			y = hires ? 5 : 2;
+		}
+
+		if (samples_left)
+		{
+			str = va("Samples needed for correct results: %d", samples_left);
+			flags |= V_REDMAP;
+		}
+		else
+		{
+			str = va("Showing the %s of %d samples.",
+					descriptor_names[cv_ps_descriptor.value - 1], cv_ps_samplesize.value);
+			flags |= V_GREENMAP;
+		}
 
-		draw_row = 15;
+		if (hires)
+			V_DrawSmallString(x, y, flags, str);
+		else
+			V_DrawThinString(x, y, flags, str);
 	}
-	else
+}
+
+static void PS_DrawRenderStats(void)
+{
+	const boolean hires = PS_HighResolution();
+	const int half_row = hires ? 5 : 4;
+	int x, y;
+
+	PS_DrawDescriptorHeader();
+
+	y = PS_DrawPerfRows(20, 10, V_YELLOWMAP, rendertime_rows);
+
+	PS_DrawPerfRows(20, y + half_row, V_GRAYMAP, gamelogicbrief_row);
+
+	if (PS_IsLevelActive())
 	{
-		draw_row = 10;
+		x = hires ? 115 : 90;
+		PS_DrawPerfRows(x, 10, V_BLUEMAP, commoncounter_rows);
+
+#ifdef HWRENDER
+		if (rendermode == render_opengl && cv_glbatching.value)
+		{
+			x = hires ? 200 : 155;
+			y = PS_DrawPerfRows(x, 10, V_PURPLEMAP, batchcount_rows);
+
+			x = hires ? 200 : 220;
+			y = hires ? y + half_row : 10;
+			PS_DrawPerfRows(x, y, V_PURPLEMAP, batchcalls_rows);
+		}
+#endif
 	}
+}
+
+static void PS_DrawGameLogicStats(void)
+{
+	const boolean hires = PS_HighResolution();
+	int x, y;
+
+	PS_DrawDescriptorHeader();
+
+	PS_DrawPerfRows(20, 10, V_YELLOWMAP, gamelogic_rows);
 
-	M_DrawPerfCount(&misc_calls_col);
+	x = hires ? 115 : 90;
+	PS_DrawPerfRows(x, 10, V_BLUEMAP, thinkercount_rows);
+
+	if (hires)
+		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");
+
+	x = hires ? 216 : 170;
+	y = hires ? 15 : 10;
+	PS_DrawPerfRows(x, y, V_PURPLEMAP, misc_calls_rows);
 }
 
-void M_DrawPerfStats(void)
+static void PS_DrawThinkFrameStats(void)
 {
 	char s[100];
+	int i;
+	// text writing position
+	int x = 2;
+	int y = 4;
+	UINT32 text_color;
+	char tempbuffer[LUA_IDSIZE];
+	char last_mod_name[LUA_IDSIZE];
+	last_mod_name[0] = '\0';
+
+	PS_DrawDescriptorHeader();
+
+	for (i = 0; i < thinkframe_hooks_length; i++)
+	{
 
-	PS_SetFrameTime();
+#define NEXT_ROW() \
+y += 4; \
+if (y > 192) \
+{ \
+	y = 4; \
+	x += 106; \
+	if (x > 214) \
+		break; \
+}
+
+		char* str = thinkframe_hooks[i].short_src;
+		char* tempstr = tempbuffer;
+		int len = (int)strlen(str);
+		char* str_ptr;
+		if (strcmp(".lua", str + len - 4) == 0)
+		{
+			str[len-4] = '\0'; // remove .lua at end
+			len -= 4;
+		}
+		// we locate the wad/pk3 name in the string and compare it to
+		// what we found on the previous iteration.
+		// if the name has changed, print it out on the screen
+		strcpy(tempstr, str);
+		str_ptr = strrchr(tempstr, '|');
+		if (str_ptr)
+		{
+			*str_ptr = '\0';
+			str = str_ptr + 1; // this is the name of the hook without the mod file name
+			str_ptr = strrchr(tempstr, PATHSEP[0]);
+			if (str_ptr)
+				tempstr = str_ptr + 1;
+			// tempstr should now point to the mod name, (wad/pk3) possibly truncated
+			if (strcmp(tempstr, last_mod_name) != 0)
+			{
+				strcpy(last_mod_name, tempstr);
+				len = (int)strlen(tempstr);
+				if (len > 25)
+					tempstr += len - 25;
+				snprintf(s, sizeof s - 1, "%s", tempstr);
+				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
+				NEXT_ROW()
+			}
+			text_color = V_YELLOWMAP;
+		}
+		else
+		{
+			// probably a standalone lua file
+			// cut off the folder if it's there
+			str_ptr = strrchr(tempstr, PATHSEP[0]);
+			if (str_ptr)
+				str = str_ptr + 1;
+			text_color = 0; // white
+		}
+		len = (int)strlen(str);
+		if (len > 20)
+			str += len - 20;
+		snprintf(s, sizeof s - 1, "%20s: %d", str,
+				PS_GetMetricScreenValue(&thinkframe_hooks[i].time_taken, true));
+		V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
+		NEXT_ROW()
+
+#undef NEXT_ROW
+
+	}
+}
 
+void M_DrawPerfStats(void)
+{
 	if (cv_perfstats.value == 1) // rendering
 	{
-		M_DrawRenderStats();
+		PS_UpdateFrameStats();
+		PS_DrawRenderStats();
 	}
 	else if (cv_perfstats.value == 2) // logic
 	{
-		M_DrawTickStats();
+		// PS_UpdateTickStats is called in TryRunTics, since otherwise it would miss
+		// tics when frame skips happen
+		PS_DrawGameLogicStats();
 	}
 	else if (cv_perfstats.value == 3) // lua thinkframe
 	{
-		if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+		if (!PS_IsLevelActive())
 			return;
-		if (vid.width < 640 || vid.height < 400) // low resolution
+		if (!PS_HighResolution())
 		{
-			// it's not gonna fit very well..
-			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not available for resolutions below 640x400");
+			// Low resolutions can't really use V_DrawSmallString that is used by thinkframe stats.
+			// A low-res version using V_DrawThinString could be implemented,
+			// but it would have much less space for information.
+			V_DrawThinString(80, 92, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Perfstats 3 is not available");
+			V_DrawThinString(80, 100, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "for resolutions below 640x400.");
 		}
-		else // high resolution
+		else
 		{
-			int i;
-			// text writing position
-			int x = 2;
-			int y = 4;
-			UINT32 text_color;
-			char tempbuffer[LUA_IDSIZE];
-			char last_mod_name[LUA_IDSIZE];
-			last_mod_name[0] = '\0';
-			for (i = 0; i < thinkframe_hooks_length; i++)
-			{
-				char* str = thinkframe_hooks[i].short_src;
-				char* tempstr = tempbuffer;
-				int len = (int)strlen(str);
-				char* str_ptr;
-				if (strcmp(".lua", str + len - 4) == 0)
-				{
-					str[len-4] = '\0'; // remove .lua at end
-					len -= 4;
-				}
-				// we locate the wad/pk3 name in the string and compare it to
-				// what we found on the previous iteration.
-				// if the name has changed, print it out on the screen
-				strcpy(tempstr, str);
-				str_ptr = strrchr(tempstr, '|');
-				if (str_ptr)
-				{
-					*str_ptr = '\0';
-					str = str_ptr + 1; // this is the name of the hook without the mod file name
-					str_ptr = strrchr(tempstr, PATHSEP[0]);
-					if (str_ptr)
-						tempstr = str_ptr + 1;
-					// tempstr should now point to the mod name, (wad/pk3) possibly truncated
-					if (strcmp(tempstr, last_mod_name) != 0)
-					{
-						strcpy(last_mod_name, tempstr);
-						len = (int)strlen(tempstr);
-						if (len > 25)
-							tempstr += len - 25;
-						snprintf(s, sizeof s - 1, "%s", tempstr);
-						V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
-						y += 4; // repeated code!
-						if (y > 192)
-						{
-							y = 4;
-							x += 106;
-							if (x > 214)
-								break;
-						}
-					}
-					text_color = V_YELLOWMAP;
-				}
-				else
-				{
-					// probably a standalone lua file
-					// cut off the folder if it's there
-					str_ptr = strrchr(tempstr, PATHSEP[0]);
-					if (str_ptr)
-						str = str_ptr + 1;
-					text_color = 0; // white
-				}
-				len = (int)strlen(str);
-				if (len > 20)
-					str += len - 20;
-				snprintf(s, sizeof s - 1, "%20s: %d", str, I_PreciseToMicros(thinkframe_hooks[i].time_taken));
-				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
-				y += 4; // repeated code!
-				if (y > 192)
-				{
-					y = 4;
-					x += 106;
-					if (x > 214)
-						break;
-				}
-			}
+			PS_DrawThinkFrameStats();
 		}
 	}
 }
+
+// remove and unallocate history from all metrics
+static void PS_ClearHistory(void)
+{
+	int i;
+
+	Z_FreeTag(PU_PERFSTATS);
+	// thinkframe hook metric history pointers need to be cleared manually
+	for (i = 0; i < thinkframe_hooks_length; i++)
+	{
+		thinkframe_hooks[i].time_taken.history = NULL;
+	}
+
+	ps_frame_index = ps_tick_index = 0;
+	// PS_UpdateMetricHistory will set these correctly when it runs
+	ps_frame_samples_left = ps_tick_samples_left = 0;
+}
+
+void PS_PerfStats_OnChange(void)
+{
+	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
+		PS_ClearHistory();
+}
+
+void PS_SampleSize_OnChange(void)
+{
+	if (cv_ps_samplesize.value > 1)
+		PS_ClearHistory();
+}
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
index 71208fbc19..3ff0e6c6b0 100644
--- a/src/m_perfstats.h
+++ b/src/m_perfstats.h
@@ -16,26 +16,45 @@
 #include "lua_script.h"
 #include "p_local.h"
 
-extern precise_t ps_tictime;
-
-extern precise_t ps_playerthink_time;
-extern precise_t ps_thinkertime;
-
-extern precise_t ps_thlist_times[];
-
-extern int       ps_checkposition_calls;
-
-extern precise_t ps_lua_thinkframe_time;
-extern int       ps_lua_mobjhooks;
+typedef struct
+{
+	union {
+		precise_t p;
+		INT32 i;
+	} value;
+	void *history;
+} ps_metric_t;
 
 typedef struct
 {
-	precise_t time_taken;
+	ps_metric_t time_taken;
 	char short_src[LUA_IDSIZE];
 } ps_hookinfo_t;
 
+#define PS_START_TIMING(metric) metric.value.p = I_GetPreciseTime()
+#define PS_STOP_TIMING(metric) metric.value.p = I_GetPreciseTime() - metric.value.p
+
+extern ps_metric_t ps_tictime;
+
+extern ps_metric_t ps_playerthink_time;
+extern ps_metric_t ps_thinkertime;
+
+extern ps_metric_t ps_thlist_times[];
+
+extern ps_metric_t ps_checkposition_calls;
+
+extern ps_metric_t ps_lua_thinkframe_time;
+extern ps_metric_t ps_lua_mobjhooks;
+
+extern ps_metric_t ps_otherlogictime;
+
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
 
+void PS_UpdateTickStats(void);
+
 void M_DrawPerfStats(void);
 
+void PS_PerfStats_OnChange(void);
+void PS_SampleSize_OnChange(void);
+
 #endif
diff --git a/src/p_map.c b/src/p_map.c
index e55bebb9a7..1d3197fabd 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -2029,7 +2029,7 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 	subsector_t *newsubsec;
 	boolean blockval = true;
 
-	ps_checkposition_calls++;
+	ps_checkposition_calls.value.i++;
 
 	I_Assert(thing != NULL);
 #ifdef PARANOIA
diff --git a/src/p_tick.c b/src/p_tick.c
index d7357eb828..1cdea9ac68 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -323,7 +323,7 @@ static inline void P_RunThinkers(void)
 	size_t i;
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
-		ps_thlist_times[i] = I_GetPreciseTime();
+		PS_START_TIMING(ps_thlist_times[i]);
 		for (currentthinker = thlist[i].next; currentthinker != &thlist[i]; currentthinker = currentthinker->next)
 		{
 #ifdef PARANOIA
@@ -331,7 +331,7 @@ static inline void P_RunThinkers(void)
 #endif
 			currentthinker->function.acp1(currentthinker);
 		}
-		ps_thlist_times[i] = I_GetPreciseTime() - ps_thlist_times[i];
+		PS_STOP_TIMING(ps_thlist_times[i]);
 	}
 
 }
@@ -653,16 +653,16 @@ void P_Ticker(boolean run)
 			}
 		}
 
-		ps_lua_mobjhooks = 0;
-		ps_checkposition_calls = 0;
+		ps_lua_mobjhooks.value.i = 0;
+		ps_checkposition_calls.value.i = 0;
 
 		LUA_HOOK(PreThinkFrame);
 
-		ps_playerthink_time = I_GetPreciseTime();
+		PS_START_TIMING(ps_playerthink_time);
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerThink(&players[i]);
-		ps_playerthink_time = I_GetPreciseTime() - ps_playerthink_time;
+		PS_STOP_TIMING(ps_playerthink_time);
 	}
 
 	// Keep track of how long they've been playing!
@@ -677,18 +677,18 @@ void P_Ticker(boolean run)
 
 	if (run)
 	{
-		ps_thinkertime = I_GetPreciseTime();
+		PS_START_TIMING(ps_thinkertime);
 		P_RunThinkers();
-		ps_thinkertime = I_GetPreciseTime() - ps_thinkertime;
+		PS_STOP_TIMING(ps_thinkertime);
 
 		// Run any "after all the other thinkers" stuff
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerAfterThink(&players[i]);
 
-		ps_lua_thinkframe_time = I_GetPreciseTime();
+		PS_START_TIMING(ps_lua_thinkframe_time);
 		LUA_HookThinkFrame();
-		ps_lua_thinkframe_time = I_GetPreciseTime() - ps_lua_thinkframe_time;
+		PS_STOP_TIMING(ps_lua_thinkframe_time);
 	}
 
 	// Run shield positioning
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 5acd4e70c5..b8559d39e5 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -804,7 +804,7 @@ static void R_AddPolyObjects(subsector_t *sub)
 	}
 
 	// for render stats
-	ps_numpolyobjects += numpolys;
+	ps_numpolyobjects.value.i += numpolys;
 
 	// sort polyobjects
 	R_SortPolyObjects(sub);
@@ -1239,7 +1239,7 @@ void R_RenderBSPNode(INT32 bspnum)
 	node_t *bsp;
 	INT32 side;
 
-	ps_numbspcalls++;
+	ps_numbspcalls.value.i++;
 
 	while (!(bspnum & NF_SUBSECTOR))  // Found a subsector?
 	{
diff --git a/src/r_main.c b/src/r_main.c
index 17e124cb92..8729b5dcb3 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -101,21 +101,22 @@ extracolormap_t *extra_colormaps = NULL;
 
 // Render stats
 precise_t ps_prevframetime = 0;
-precise_t ps_rendercalltime = 0;
-precise_t ps_uitime = 0;
-precise_t ps_swaptime = 0;
+ps_metric_t ps_rendercalltime = {0};
+ps_metric_t ps_otherrendertime = {0};
+ps_metric_t ps_uitime = {0};
+ps_metric_t ps_swaptime = {0};
 
-precise_t ps_bsptime = 0;
+ps_metric_t ps_bsptime = {0};
 
-precise_t ps_sw_spritecliptime = 0;
-precise_t ps_sw_portaltime = 0;
-precise_t ps_sw_planetime = 0;
-precise_t ps_sw_maskedtime = 0;
+ps_metric_t ps_sw_spritecliptime = {0};
+ps_metric_t ps_sw_portaltime = {0};
+ps_metric_t ps_sw_planetime = {0};
+ps_metric_t ps_sw_maskedtime = {0};
 
-int ps_numbspcalls = 0;
-int ps_numsprites = 0;
-int ps_numdrawnodes = 0;
-int ps_numpolyobjects = 0;
+ps_metric_t ps_numbspcalls = {0};
+ps_metric_t ps_numsprites = {0};
+ps_metric_t ps_numdrawnodes = {0};
+ps_metric_t ps_numpolyobjects = {0};
 
 static CV_PossibleValue_t drawdist_cons_t[] = {
 	{256, "256"},	{512, "512"},	{768, "768"},
@@ -1496,11 +1497,11 @@ void R_RenderPlayerView(player_t *player)
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
-	ps_numbspcalls = ps_numpolyobjects = ps_numdrawnodes = 0;
-	ps_bsptime = I_GetPreciseTime();
+	ps_numbspcalls.value.i = ps_numpolyobjects.value.i = ps_numdrawnodes.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 	R_RenderBSPNode((INT32)numnodes - 1);
-	ps_bsptime = I_GetPreciseTime() - ps_bsptime;
-	ps_numsprites = visspritecount;
+	PS_STOP_TIMING(ps_bsptime);
+	ps_numsprites.value.i = visspritecount;
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
 	mytotal += mycount; // 64bit add
@@ -1510,9 +1511,9 @@ void R_RenderPlayerView(player_t *player)
 //profile stuff ---------------------------------------------------------
 	Mask_Post(&masks[nummasks - 1]);
 
-	ps_sw_spritecliptime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_spritecliptime);
 	R_ClipSprites(drawsegs, NULL);
-	ps_sw_spritecliptime = I_GetPreciseTime() - ps_sw_spritecliptime;
+	PS_STOP_TIMING(ps_sw_spritecliptime);
 
 
 	// Add skybox portals caused by sky visplanes.
@@ -1520,7 +1521,7 @@ void R_RenderPlayerView(player_t *player)
 		Portal_AddSkyboxPortals();
 
 	// Portal rendering. Hijacks the BSP traversal.
-	ps_sw_portaltime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_portaltime);
 	if (portal_base)
 	{
 		portal_t *portal;
@@ -1560,17 +1561,17 @@ void R_RenderPlayerView(player_t *player)
 			Portal_Remove(portal);
 		}
 	}
-	ps_sw_portaltime = I_GetPreciseTime() - ps_sw_portaltime;
+	PS_STOP_TIMING(ps_sw_portaltime);
 
-	ps_sw_planetime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_planetime);
 	R_DrawPlanes();
-	ps_sw_planetime = I_GetPreciseTime() - ps_sw_planetime;
+	PS_STOP_TIMING(ps_sw_planetime);
 
 	// draw mid texture and sprite
 	// And now 3D floors/sides!
-	ps_sw_maskedtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_maskedtime);
 	R_DrawMasked(masks, nummasks);
-	ps_sw_maskedtime = I_GetPreciseTime() - ps_sw_maskedtime;
+	PS_STOP_TIMING(ps_sw_maskedtime);
 
 	free(masks);
 }
diff --git a/src/r_main.h b/src/r_main.h
index f81447c456..5f3bed9803 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -17,6 +17,7 @@
 #include "d_player.h"
 #include "r_data.h"
 #include "r_textures.h"
+#include "m_perfstats.h" // ps_metric_t
 
 //
 // POV related.
@@ -79,21 +80,22 @@ boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixe
 // Render stats
 
 extern precise_t ps_prevframetime;// time when previous frame was rendered
-extern precise_t ps_rendercalltime;
-extern precise_t ps_uitime;
-extern precise_t ps_swaptime;
-
-extern precise_t ps_bsptime;
-
-extern precise_t ps_sw_spritecliptime;
-extern precise_t ps_sw_portaltime;
-extern precise_t ps_sw_planetime;
-extern precise_t ps_sw_maskedtime;
-
-extern int ps_numbspcalls;
-extern int ps_numsprites;
-extern int ps_numdrawnodes;
-extern int ps_numpolyobjects;
+extern ps_metric_t ps_rendercalltime;
+extern ps_metric_t ps_otherrendertime;
+extern ps_metric_t ps_uitime;
+extern ps_metric_t ps_swaptime;
+
+extern ps_metric_t ps_bsptime;
+
+extern ps_metric_t ps_sw_spritecliptime;
+extern ps_metric_t ps_sw_portaltime;
+extern ps_metric_t ps_sw_planetime;
+extern ps_metric_t ps_sw_maskedtime;
+
+extern ps_metric_t ps_numbspcalls;
+extern ps_metric_t ps_numsprites;
+extern ps_metric_t ps_numdrawnodes;
+extern ps_metric_t ps_numpolyobjects;
 
 //
 // REFRESH - the actual rendering functions.
diff --git a/src/r_things.c b/src/r_things.c
index ea57e40860..bed71a6d79 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -2745,7 +2745,7 @@ static drawnode_t *R_CreateDrawNode(drawnode_t *link)
 	node->ffloor = NULL;
 	node->sprite = NULL;
 
-	ps_numdrawnodes++;
+	ps_numdrawnodes.value.i++;
 	return node;
 }
 
diff --git a/src/z_zone.h b/src/z_zone.h
index be55bf9940..17f572a905 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -39,6 +39,7 @@ enum
 	// Tags < PU_LEVEL are not purged until freed explicitly.
 	PU_STATIC                = 1, // static entire execution time
 	PU_LUA                   = 2, // static entire execution time -- used by lua so it doesn't get caught in loops forever
+	PU_PERFSTATS             = 3, // static between changes to ps_samplesize cvar
 
 	PU_SOUND                 = 11, // static while playing
 	PU_MUSIC                 = 12, // static while playing
-- 
GitLab