UserInterface: poll the network stream on the 60 Hz update clock

40250 runs CPythonApplication::Process every 16/17 ms and only skips the
render when late, so GamePhase gets its three-packets-a-poll budget 60
times a second. The host polled once per rendered frame: at 30 fps idle
(or a slow phone) mob move packets queued behind character add/del
bursts for up to a second and mobs stopped, then jumped. Poll once per
elapsed 60 Hz tick. Real-server A/B: move push gap p50 138-178 -> 70-92
ms, p90 580-785 -> 358-403 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 18:36:49 +09:00
co-authored by Claude Opus 5.5
parent 3aa4c482e9
commit 8ec8f1a445
2 changed files with 25 additions and 1 deletions
+8
View File
@@ -684,6 +684,14 @@
"tests": [ "tests": [
"project/app_flow_lifecycle_test.gd" "project/app_flow_lifecycle_test.gd"
] ]
},
{
"reference": "CPythonApplication::Process on the 60 Hz s_uiNextFrameTime update clock; a late frame skips only the render (s_bFrameSkip), so CPythonNetworkStream::Process runs every 16/17 ms",
"implementation": "src/platform/UserInterface/PythonApplication.cpp CPythonApplication::Process: one host frame polls the stream once per elapsed 60 Hz tick (catch-up capped at 500 ms)",
"invariant": "GamePhase packet budget (3 packets a poll below 8 KB pending) must be 180/s regardless of render rate; at 30 fps idle it was 90/s and mob move packets queued behind character add/del bursts",
"tests": [
"real server 192.168.21.203 chase probe A/B 2026-09-30: GC_MOVE push gap p50 138-178 -> 70-92 ms, p90 580-785 -> 358-403 ms"
]
} }
], ],
"evidence": { "evidence": {
@@ -330,7 +330,23 @@ bool CPythonApplication::Process()
// Network I/O // Network I/O
{ {
MtPerf::CScope kPerf(MtPerf::SECTION_NETWORK); MtPerf::CScope kPerf(MtPerf::SECTION_NETWORK);
CPythonNetworkStream::Instance().Process(); // PORT: 40250 runs Process() on its 60 Hz update clock and only skips the render pass when late
// (s_bFrameSkip), so the stream is polled every 16 ms whatever the render rate, and GamePhase
// handles three packets a poll while less than 8 KB is pending. The host runs one Process() per
// rendered frame (30 fps when idle, less on a busy phone), which halves that budget or worse: the
// character add/del bursts of a mob field hold the move packets back for up to a second and the
// mobs stop, then jump. Poll once per elapsed 60 Hz tick, as 40250 does.
{
static DWORD s_dwNextNetworkPoll = 0;
const DWORD dwNow = ELTimer_GetMSec();
if (!s_dwNextNetworkPoll || int(dwNow - s_dwNextNetworkPoll) > 500)
s_dwNextNetworkPoll = dwNow;
do
{
CPythonNetworkStream::Instance().Process();
s_dwNextNetworkPoll += 16 + (s_dwNextNetworkPoll & 1);
} while (int(dwNow - s_dwNextNetworkPoll) >= 0);
}
// PORT: m_kGuildMarkUploader/m_kGuildMarkDownloader.Process() follow here in 40250; the guild mark // PORT: m_kGuildMarkUploader/m_kGuildMarkDownloader.Process() follow here in 40250; the guild mark
// transfer is not ported yet (GuildMarkDownloader.cpp/GuildMarkUploader.cpp). // transfer is not ported yet (GuildMarkDownloader.cpp/GuildMarkUploader.cpp).
CAccountConnector::Instance().Process(); CAccountConnector::Instance().Process();