diff --git a/src/lua_hook.h b/src/lua_hook.h
index cb0e30d870ad3672d1c6076627ff73b90b103d6b..026e5c4aaf97c54326a22d5de56f4e44cbddd09f 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -126,7 +126,9 @@ int  LUA_HookPlayer(player_t *, int hook);
 int  LUA_HookTiccmd(player_t *, ticcmd_t *, int hook);
 int  LUA_HookKey(event_t *event, int hook); // Hooks for key events
 
+void LUA_HookPreThinkFrame(void);
 void LUA_HookThinkFrame(void);
+void LUA_HookPostThinkFrame(void);
 int  LUA_HookMobjLineCollide(mobj_t *, line_t *);
 int  LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher);
 int  LUA_HookShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 38815a06cfa4558b8ad5659bcbe1cdba5705ca03..529c189ff7883bbd377327ad5b44d2ae5af4e196 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -671,10 +671,8 @@ void LUA_HookHUD(int hook_type, huddrawlist_h list)
                                SPECIALIZED HOOKS
    ========================================================================= */
 
-void LUA_HookThinkFrame(void)
+static void hook_think_frame(int type)
 {
-	const int type = HOOK(ThinkFrame);
-
 	// variables used by perf stats
 	int hook_index = 0;
 	precise_t time_taken = 0;
@@ -692,7 +690,7 @@ void LUA_HookThinkFrame(void)
 		{
 			get_hook(&hook, map->ids, k);
 
-			if (cv_perfstats.value == 3)
+			if (cv_perfstats.value >= 3)
 			{
 				lua_pushvalue(gL, -1);/* need the function again */
 				time_taken = I_GetPreciseTime();
@@ -700,12 +698,18 @@ void LUA_HookThinkFrame(void)
 
 			call_single_hook(&hook);
 
-			if (cv_perfstats.value == 3)
+			if (cv_perfstats.value >= 3)
 			{
 				lua_Debug ar;
 				time_taken = I_GetPreciseTime() - time_taken;
 				lua_getinfo(gL, ">S", &ar);
-				PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
+				if (type == 4) // sorry for magic numbers
+					PS_SetPreThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
+				else if (type == 5)
+					PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
+				else if (type == 6)
+					PS_SetPostThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
+				
 				hook_index++;
 			}
 		}
@@ -714,6 +718,21 @@ void LUA_HookThinkFrame(void)
 	}
 }
 
+void LUA_HookPreThinkFrame(void)
+{
+	hook_think_frame(HOOK(PreThinkFrame));
+}
+
+void LUA_HookThinkFrame(void)
+{
+	hook_think_frame(HOOK(ThinkFrame));
+}
+
+void LUA_HookPostThinkFrame(void)
+{
+	hook_think_frame(HOOK(PostThinkFrame));
+}
+
 int LUA_HookMobjLineCollide(mobj_t *mobj, line_t *line)
 {
 	Hook_State hook;
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 1511859324059a5ce8e5836ff99dbdbe841c02a1..b9948bdc0c3284a3473e7e6e8b95b2e3bd8fb568 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -65,7 +65,10 @@ static ps_metric_t ps_removecount = {0};
 
 ps_metric_t ps_checkposition_calls = {0};
 
+ps_metric_t ps_lua_prethinkframe_time = {0};
 ps_metric_t ps_lua_thinkframe_time = {0};
+ps_metric_t ps_lua_postthinkframe_time = {0};
+
 ps_metric_t ps_lua_mobjhooks = {0};
 
 ps_metric_t ps_otherlogictime = {0};
@@ -157,7 +160,9 @@ perfstatrow_t gamelogic_rows[] = {
 	{"  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},
+	{" lprethinkf", " LUAh_PreThinkFrame:", &ps_lua_prethinkframe_time, PS_TIME|PS_LEVEL},
 	{" lthinkf", " LUAh_ThinkFrame:", &ps_lua_thinkframe_time, PS_TIME|PS_LEVEL},
+	{" lpostthinkf", " LUAh_PostThinkFrame:", &ps_lua_postthinkframe_time, PS_TIME|PS_LEVEL},
 	{" other  ", " Other:          ", &ps_otherlogictime, PS_TIME|PS_LEVEL},
 	{0}
 };
@@ -192,10 +197,43 @@ int ps_frame_index = 0;
 int ps_tick_index = 0;
 
 // dynamically allocated resizeable array for thinkframe hook stats
+ps_hookinfo_t *prethinkframe_hooks = NULL;
+int prethinkframe_hooks_length = 0;
+int prethinkframe_hooks_capacity = 16;
+
 ps_hookinfo_t *thinkframe_hooks = NULL;
 int thinkframe_hooks_length = 0;
 int thinkframe_hooks_capacity = 16;
 
+ps_hookinfo_t *postthinkframe_hooks = NULL;
+int postthinkframe_hooks_length = 0;
+int postthinkframe_hooks_capacity = 16;
+
+void PS_SetPreThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
+{
+	if (!prethinkframe_hooks)
+	{
+		// array needs to be initialized
+		prethinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * prethinkframe_hooks_capacity, PU_STATIC, NULL);
+	}
+	if (index >= prethinkframe_hooks_capacity)
+	{
+		// array needs more space, realloc with double size
+		int new_capacity = prethinkframe_hooks_capacity * 2;
+		prethinkframe_hooks = Z_Realloc(prethinkframe_hooks,
+			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
+		// initialize new memory with zeros so the pointers in the structs are null
+		memset(&prethinkframe_hooks[prethinkframe_hooks_capacity], 0,
+			sizeof(ps_hookinfo_t) * prethinkframe_hooks_capacity);
+		prethinkframe_hooks_capacity = new_capacity;
+	}
+	prethinkframe_hooks[index].time_taken.value.p = time_taken;
+	memcpy(prethinkframe_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
+	prethinkframe_hooks_length = index + 1;
+}
+
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
 {
 	if (!thinkframe_hooks)
@@ -221,6 +259,31 @@ void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
 	thinkframe_hooks_length = index + 1;
 }
 
+void PS_SetPostThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
+{
+	if (!postthinkframe_hooks)
+	{
+		// array needs to be initialized
+		postthinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * postthinkframe_hooks_capacity, PU_STATIC, NULL);
+	}
+	if (index >= postthinkframe_hooks_capacity)
+	{
+		// array needs more space, realloc with double size
+		int new_capacity = postthinkframe_hooks_capacity * 2;
+		postthinkframe_hooks = Z_Realloc(postthinkframe_hooks,
+			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
+		// initialize new memory with zeros so the pointers in the structs are null
+		memset(&postthinkframe_hooks[postthinkframe_hooks_capacity], 0,
+			sizeof(ps_hookinfo_t) * postthinkframe_hooks_capacity);
+		postthinkframe_hooks_capacity = new_capacity;
+	}
+	postthinkframe_hooks[index].time_taken.value.p = time_taken;
+	memcpy(postthinkframe_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
+	postthinkframe_hooks_length = index + 1;
+}
+
 static boolean PS_HighResolution(void)
 {
 	return (vid.width >= 640 && vid.height >= 400);
@@ -564,7 +627,9 @@ void PS_UpdateTickStats(void)
 				ps_tictime.value.p -
 				ps_playerthink_time.value.p -
 				ps_thinkertime.value.p -
-				ps_lua_thinkframe_time.value.p;
+				ps_lua_prethinkframe_time.value.p -
+				ps_lua_thinkframe_time.value.p -
+				ps_lua_postthinkframe_time.value.p;
 
 			PS_CountThinkers();
 		}
@@ -576,21 +641,35 @@ void PS_UpdateTickStats(void)
 			PS_UpdateRowHistories(misc_calls_rows, false);
 		}
 	}
-	if (cv_perfstats.value == 3 && cv_ps_samplesize.value > 1 && PS_IsLevelActive())
+	if (cv_ps_samplesize.value > 1)
 	{
-		int i;
-		for (i = 0; i < thinkframe_hooks_length; i++)
+		if(cv_perfstats.value >= 3 && PS_IsLevelActive())
 		{
-			PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false, false);
+			int i;
+			if (cv_perfstats.value == 3)
+			{
+				for (i = 0; i < thinkframe_hooks_length; i++)
+					PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false, false);
+			}
+			else if (cv_perfstats.value == 4)
+			{
+				for (i = 0; i < prethinkframe_hooks_length; i++)
+					PS_UpdateMetricHistory(&prethinkframe_hooks[i].time_taken, true, false, false);
+			}
+			else if (cv_perfstats.value == 5)
+			{
+				for (i = 0; i < postthinkframe_hooks_length; i++)
+					PS_UpdateMetricHistory(&postthinkframe_hooks[i].time_taken, true, false, false);
+			}
+		}
+		if (cv_perfstats.value)
+		{
+			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 (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--;
 	}
 }
 
@@ -610,7 +689,7 @@ static void PS_DrawDescriptorHeader(void)
 		int samples_left = max(ps_frame_samples_left, ps_tick_samples_left);
 		int x, y;
 
-		if (cv_perfstats.value == 3)
+		if (cv_perfstats.value >= 3)
 		{
 			x = 2;
 			y = 0;
@@ -697,7 +776,7 @@ static void PS_DrawGameLogicStats(void)
 	PS_DrawPerfRows(x, y, V_PURPLEMAP, misc_calls_rows);
 }
 
-static void PS_DrawThinkFrameStats(void)
+static void draw_think_frame_stats(int hook_length, ps_hookinfo_t *hook)
 {
 	char s[100];
 	int i;
@@ -711,7 +790,7 @@ static void PS_DrawThinkFrameStats(void)
 
 	PS_DrawDescriptorHeader();
 
-	for (i = 0; i < thinkframe_hooks_length; i++)
+	for (i = 0; i < hook_length; i++)
 	{
 
 #define NEXT_ROW() \
@@ -724,7 +803,7 @@ if (y > 192) \
 		break; \
 }
 
-		char* str = thinkframe_hooks[i].short_src;
+		char* str = hook[i].short_src;
 		char* tempstr = tempbuffer;
 		int len = (int)strlen(str);
 		char* str_ptr;
@@ -771,7 +850,7 @@ if (y > 192) \
 		if (len > 20)
 			str += len - 20;
 		snprintf(s, sizeof s - 1, "%20s: %d", str,
-				PS_GetMetricScreenValue(&thinkframe_hooks[i].time_taken, true));
+				PS_GetMetricScreenValue(&hook[i].time_taken, true));
 		V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
 		NEXT_ROW()
 
@@ -780,6 +859,21 @@ if (y > 192) \
 	}
 }
 
+static void PS_DrawPreThinkFrameStats(void)
+{
+	draw_think_frame_stats(prethinkframe_hooks_length, prethinkframe_hooks);
+}
+
+static void PS_DrawThinkFrameStats(void)
+{
+	draw_think_frame_stats(thinkframe_hooks_length, thinkframe_hooks);
+}
+
+static void PS_DrawPostThinkFrameStats(void)
+{
+	draw_think_frame_stats(postthinkframe_hooks_length, postthinkframe_hooks);
+}
+
 void M_DrawPerfStats(void)
 {
 	if (cv_perfstats.value == 1) // rendering
@@ -793,7 +887,7 @@ void M_DrawPerfStats(void)
 		// tics when frame skips happen
 		PS_DrawGameLogicStats();
 	}
-	else if (cv_perfstats.value == 3) // lua thinkframe
+	else if (cv_perfstats.value >= 3) // lua thinkframe
 	{
 		if (!PS_IsLevelActive())
 			return;
@@ -802,13 +896,22 @@ void M_DrawPerfStats(void)
 			// 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, 92, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Lua Perfstats is not available");
 			V_DrawThinString(80, 100, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "for resolutions below 640x400.");
+			return;
 		}
-		else
+		if (cv_perfstats.value == 3)
 		{
 			PS_DrawThinkFrameStats();
 		}
+		else if (cv_perfstats.value == 4)
+		{
+			PS_DrawPreThinkFrameStats();
+		}
+		else if (cv_perfstats.value == 5)
+		{
+			PS_DrawPostThinkFrameStats();
+		}
 	}
 }
 
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
index db250be2a273a970d9209d343f16f948d7d53231..592ab31d2d62adac2704b740e40f5476158e38c6 100644
--- a/src/m_perfstats.h
+++ b/src/m_perfstats.h
@@ -43,12 +43,16 @@ extern ps_metric_t ps_thlist_times[];
 
 extern ps_metric_t ps_checkposition_calls;
 
+extern ps_metric_t ps_lua_prethinkframe_time;
 extern ps_metric_t ps_lua_thinkframe_time;
+extern ps_metric_t ps_lua_postthinkframe_time;
 extern ps_metric_t ps_lua_mobjhooks;
 
 extern ps_metric_t ps_otherlogictime;
 
+void PS_SetPreThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
+void PS_SetPostThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
 
 void PS_UpdateTickStats(void);
 
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index ef1ef9aeb1971d1a63b41944e59c161d8e05f676..dec1ef5a01fcc3c277d9d22a519f8d6e6a49e8b0 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -380,7 +380,7 @@ consvar_t cv_mute = CVAR_INIT ("mute", "Off", CV_NETVAR|CV_CALL|CV_ALLOWLUA, CV_
 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}};
+	{0, "Off"}, {1, "Rendering"}, {2, "Logic"}, {3, "ThinkFrame"}, {4, "PreThinkFrame"}, {5, "PostThinkFrame"}, {0, 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}};
diff --git a/src/p_tick.c b/src/p_tick.c
index dca806ebee2810b5556f8cf6d288c7ffd1607069..143fa7e36844ca01d8a6bd2b6f41aed3bb04df1e 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -769,7 +769,9 @@ void P_Ticker(boolean run)
 		ps_lua_mobjhooks.value.i = 0;
 		ps_checkposition_calls.value.i = 0;
 
-		LUA_HOOK(PreThinkFrame);
+		PS_START_TIMING(ps_lua_prethinkframe_time);
+		LUA_HookPreThinkFrame();
+		PS_STOP_TIMING(ps_lua_prethinkframe_time);
 
 		PS_START_TIMING(ps_playerthink_time);
 		for (i = 0; i < MAXPLAYERS; i++)
@@ -867,7 +869,9 @@ void P_Ticker(boolean run)
 		if (modeattacking)
 			G_GhostTicker();
 
-		LUA_HOOK(PostThinkFrame);
+		PS_START_TIMING(ps_lua_postthinkframe_time);
+		LUA_HookPostThinkFrame();
+		PS_STOP_TIMING(ps_lua_postthinkframe_time);
 	}
 
 	if (run)