Compare commits

...

3 Commits

Author SHA1 Message Date
genxium
7b878ff947 Fixes for backend Golang select-multi-channel implementation. 2023-02-21 22:07:48 +08:00
genxium
c78c480f99 Enhanced UDP session resource management. 2023-02-21 15:21:15 +08:00
genxium
b50874f5c4 Enhanced RecvRingBuff in cpp. 2023-02-21 11:54:06 +08:00
5 changed files with 59 additions and 26 deletions

View File

@@ -25,9 +25,16 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"net" "net"
// _ "net/http/pprof"
) )
func main() { func main() {
/*
// Only used for profiling
go func() {
http.ListenAndServe("0.0.0.0:6060", nil)
}()
*/
MustParseConfig() MustParseConfig()
MustParseConstants() MustParseConstants()
storage.Init() storage.Init()

View File

@@ -483,7 +483,7 @@ func (pR *Room) StartBattle() {
*/ */
totalElapsedNanos := utils.UnixtimeNano() - battleStartedAt totalElapsedNanos := utils.UnixtimeNano() - battleStartedAt
nextRenderFrameId := int32((totalElapsedNanos + pR.dilutedRollbackEstimatedDtNanos - 1) / pR.dilutedRollbackEstimatedDtNanos) // fast ceiling nextRenderFrameId := int32((totalElapsedNanos + pR.dilutedRollbackEstimatedDtNanos - 1) / pR.dilutedRollbackEstimatedDtNanos) // fast ceiling
toSleepNanos := int64(0) toSleepNanos := int64(pR.dilutedRollbackEstimatedDtNanos >> 1) // Sleep half-frame time by default
if nextRenderFrameId > pR.RenderFrameId { if nextRenderFrameId > pR.RenderFrameId {
if 0 == pR.RenderFrameId { if 0 == pR.RenderFrameId {
// It's important to send kickoff frame iff "0 == pR.RenderFrameId && nextRenderFrameId > pR.RenderFrameId", otherwise it might send duplicate kickoff frames // It's important to send kickoff frame iff "0 == pR.RenderFrameId && nextRenderFrameId > pR.RenderFrameId", otherwise it might send duplicate kickoff frames
@@ -515,7 +515,7 @@ func (pR *Room) StartBattle() {
pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano() pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano()
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation) elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
toSleepNanos = pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation // don't sleep if "nextRenderFrame == pR.RenderFrameId" toSleepNanos = pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation
if elapsedInCalculation > pR.RollbackEstimatedDtNanos { if elapsedInCalculation > pR.RollbackEstimatedDtNanos {
Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v, dilutedRollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos, pR.dilutedRollbackEstimatedDtNanos)) Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v, dilutedRollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos, pR.dilutedRollbackEstimatedDtNanos))
} }
@@ -544,13 +544,13 @@ func (pR *Room) StartBattle() {
} }
select { select {
// [WARNING] DON'T put a "default" block here! Otherwise "for { select {... default: } }" pattern would NEVER block on empty channel and thus consume a lot of CPU time unnecessarily!
case inputsBufferSnapshot := <-playerDownsyncChan: case inputsBufferSnapshot := <-playerDownsyncChan:
pR.downsyncToSinglePlayer(playerId, player, inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, inputsBufferSnapshot.ToSendInputFrameDownsyncs, inputsBufferSnapshot.ShouldForceResync) pR.downsyncToSinglePlayer(playerId, player, inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, inputsBufferSnapshot.ToSendInputFrameDownsyncs, inputsBufferSnapshot.ShouldForceResync)
//Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d)#2", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, playerId)) //Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d)#2", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, playerId))
case inputsBufferSnapshot2 := <-playerSecondaryDownsyncChan: case inputsBufferSnapshot2 := <-playerSecondaryDownsyncChan:
pR.downsyncPeerInputFrameUpsyncToSinglePlayer(playerId, player, inputsBufferSnapshot2.ToSendInputFrameDownsyncs, inputsBufferSnapshot2.PeerJoinIndex) pR.downsyncPeerInputFrameUpsyncToSinglePlayer(playerId, player, inputsBufferSnapshot2.ToSendInputFrameDownsyncs, inputsBufferSnapshot2.PeerJoinIndex)
//Logger.Info(fmt.Sprintf("Sent secondary inputsBufferSnapshot to for (roomId: %d, playerId:%d)#2", pR.Id, playerId)) //Logger.Info(fmt.Sprintf("Sent secondary inputsBufferSnapshot to for (roomId: %d, playerId:%d)#2", pR.Id, playerId))
default:
} }
} }
} }

View File

@@ -461,7 +461,7 @@
"array": [ "array": [
0, 0,
0, 0,
216.50635094610968, 210.43877906529718,
0, 0,
0, 0,
0, 0,

View File

@@ -33,7 +33,7 @@ SendWork* SendRingBuff::pop() {
// Recving // Recving
bool isFullWithLoadedVals(int n, int oldCnt, int oldSt, int oldEd) { bool isFullWithLoadedVals(int n, int oldCnt, int oldSt, int oldEd) {
return (n <= oldCnt) || (n > oldCnt && 0 < oldCnt && oldEd == oldSt); return (n <= oldCnt && oldEd == oldSt) || (n > oldCnt && 0 < oldCnt && oldEd == oldSt);
} }
void RecvRingBuff::put(char* newBytes, size_t newBytesLen) { void RecvRingBuff::put(char* newBytes, size_t newBytesLen) {
@@ -44,10 +44,15 @@ void RecvRingBuff::put(char* newBytes, size_t newBytesLen) {
// We want to increase the success rate of "pop()" if it's being executed by "GameThread/pollUdpRecvRingBuff", thus the below order of loading is IMPORTANT, i.e. load "cnt" first because it's decremented earlier than "st" being incremented. // We want to increase the success rate of "pop()" if it's being executed by "GameThread/pollUdpRecvRingBuff", thus the below order of loading is IMPORTANT, i.e. load "cnt" first because it's decremented earlier than "st" being incremented.
int oldCnt = cnt.load(); int oldCnt = cnt.load();
/*
[WARNING]
Note that "RecvRingBuff.st" might have decremented in "GameThread" by a successful "pop()" between "cnt.load()" and "st.load()" here in "UvRecvThread"! Therefore "n <= oldCnt" doesn't necessarily imply "oldEd == oldSt"!
*/
int oldSt = st.load(); // Used to guard against "cnt decremented in pop(...), but st not yet incremented and thus return value not yet copied to avoid contamination" int oldSt = st.load(); // Used to guard against "cnt decremented in pop(...), but st not yet incremented and thus return value not yet copied to avoid contamination"
int tried = 0; int tried = 0;
/* /*
1. When "n <= oldCnt", it's implied that "oldEd == oldSt"; 1. When "n <= oldCnt", it might still be true "oldEd != oldSt" (see the note above);
2. When "n > oldCnt", it might still be true that "oldEd == oldSt" if "pop()" hasn't successfully incremented "st" due to any reason; 2. When "n > oldCnt", it might still be true that "oldEd == oldSt" if "pop()" hasn't successfully incremented "st" due to any reason;
3. When "oldEd == oldSt", it doesn't imply anything useful, because any of the following could be true 3. When "oldEd == oldSt", it doesn't imply anything useful, because any of the following could be true
- a. "n <= oldCnt", i.e. the ringbuff is full - a. "n <= oldCnt", i.e. the ringbuff is full

View File

@@ -160,7 +160,6 @@ void startRecvLoop(void* arg) {
int uvCloseRet = uv_loop_close(l); int uvCloseRet = uv_loop_close(l);
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet); CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
uv_mutex_destroy(&recvRingBuffLock);
} }
void startSendLoop(void* arg) { void startSendLoop(void* arg) {
@@ -174,7 +173,6 @@ void startSendLoop(void* arg) {
int uvCloseRet = uv_loop_close(l); int uvCloseRet = uv_loop_close(l);
CCLOG("UDP send loop is closed in UvSendThread, uvCloseRet=%d", uvCloseRet); CCLOG("UDP send loop is closed in UvSendThread, uvCloseRet=%d", uvCloseRet);
uv_mutex_destroy(&sendRingBuffLock);
} }
int initSendLoop(struct sockaddr const* pUdpAddr) { int initSendLoop(struct sockaddr const* pUdpAddr) {
@@ -189,9 +187,6 @@ int initSendLoop(struct sockaddr const* pUdpAddr) {
uv_mutex_init(&sendRingBuffLock); uv_mutex_init(&sendRingBuffLock);
sendRingBuff = new SendRingBuff(maxBuffedMsgs); sendRingBuff = new SendRingBuff(maxBuffedMsgs);
uv_mutex_init(&recvRingBuffLock);
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig); uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend); uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
@@ -208,6 +203,9 @@ bool initRecvLoop(struct sockaddr const* pUdpAddr) {
CCLOGERROR("Failed to bind recv; recvSockInitRes=%d, recvbindRes=%d, reason=%s", recvSockInitRes, recvbindRes, uv_strerror(recvbindRes)); CCLOGERROR("Failed to bind recv; recvSockInitRes=%d, recvbindRes=%d, reason=%s", recvSockInitRes, recvbindRes, uv_strerror(recvbindRes));
exit(-1); exit(-1);
} }
uv_mutex_init(&recvRingBuffLock);
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
uv_udp_recv_start(udpRecvSocket, _allocBuffer, _onRead); uv_udp_recv_start(udpRecvSocket, _allocBuffer, _onRead);
uv_async_init(recvLoop, &uvRecvLoopStopSig, _onUvStopSig); uv_async_init(recvLoop, &uvRecvLoopStopSig, _onUvStopSig);
@@ -249,6 +247,12 @@ bool DelayNoMore::UdpSession::openUdpSession(int port) {
bool DelayNoMore::UdpSession::closeUdpSession() { bool DelayNoMore::UdpSession::closeUdpSession() {
CCLOG("About to close udp session and dealloc all resources..."); CCLOG("About to close udp session and dealloc all resources...");
/*
[WARNING] It's possible that "closeUdpSession" is called when "openUdpSession" was NEVER CALLED, thus we have to avoid program crash in this case.
In general one shouldn't just check the state of "sendTid" by whether or not "NULL == sendLoop", but in this particular game, both "openUdpSession" and "closeUdpSession" are only called from "GameThread", no thread-safety concern here, i.e. if "openUdpSession" was ever called earlier, then "sendLoop" wouldn't be NULL when "closeUdpSession" is later called.
*/
if (NULL != sendLoop) {
uv_async_send(&uvSendLoopStopSig); uv_async_send(&uvSendLoopStopSig);
CCLOG("Signaling UvSendThread to end in GameThread..."); CCLOG("Signaling UvSendThread to end in GameThread...");
uv_thread_join(&sendTid); uv_thread_join(&sendTid);
@@ -256,6 +260,14 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
free(sendLoop); free(sendLoop);
delete sendRingBuff; delete sendRingBuff;
udpSendSocket = NULL;
sendLoop = NULL;
sendRingBuff = NULL;
uv_mutex_destroy(&sendRingBuffLock);
}
if (NULL != recvLoop) {
uv_async_send(&uvRecvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send uv_async_send(&uvRecvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send
CCLOG("Signaling UvRecvThread to end in GameThread..."); CCLOG("Signaling UvRecvThread to end in GameThread...");
uv_thread_join(&recvTid); uv_thread_join(&recvTid);
@@ -263,6 +275,13 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
free(recvLoop); free(recvLoop);
delete recvRingBuff; delete recvRingBuff;
udpRecvSocket = NULL;
recvLoop = NULL;
recvRingBuff = NULL;
uv_mutex_destroy(&recvRingBuffLock);
}
CCLOG("Closed udp session and dealloc all resources in GameThread..."); CCLOG("Closed udp session and dealloc all resources in GameThread...");
return true; return true;
@@ -356,8 +375,10 @@ bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
while (true) { while (true) {
RecvWork f; RecvWork f;
bool res = recvRingBuff->pop(&f); bool res = recvRingBuff->pop(&f);
if (!res) return false; if (!res) {
// Deliberately returning "true" here to prevent "jswrapper" from printing "Failed to invoke Xxx..." too frequently
return true;
}
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine! // [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
se::AutoHandleScope hs; se::AutoHandleScope hs;
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"! // [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!