Conversation
Every visit of the admin panel live map page - and every terrain image request - permanently leaked one GameMapInfoAdapter per hosted map, because ObservableGameServerAdapter never disposed them. As the leaked adapters stayed subscribed to the ObjectAdded/ObjectRemoved events of the live game maps, they were rooted forever and additionally slowed down the game loop, since these events are awaited on every spawn, drop, despawn and map transition. * ObservableGameServerAdapter now disposes (and unsubscribes) its map infos when it's disposed itself. A map adapter which was created concurrently to the disposal is disposed as well, instead of being added to the cleared list. * InitializeAsync got a withMapInfos parameter, so callers which only need to register a map observer can skip the expensive creation of the map infos for all maps. * MapPage uses that, and resolves the map name directly at the game server context. * TerrainController doesn't create an ObservableGameServerAdapter anymore. It resolves the GameMap at the context and renders its terrain through a new GetTerrainStream overload, which resolves the TODO about the expensive adapter creation. * MapPlayerList subscribed to IMapController.PlayersChanged on every OnParametersSet - which happens on each re-render of the parent component - and never unsubscribed. It now subscribes only when the controller instance actually changed and unsubscribes when it's disposed. * Map component: removed the synchronous Dispose, which bound to Task.Run(Func<TResult>) and therefore never awaited the returned ValueTask of DisposeAsync. * MapController now awaits ObserverToWorldViewAdapter.DisposeAsync instead of calling the no-op synchronous Dispose, so its DisposeAsyncCore actually runs. Fixes #919 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XyXbCxbtDecRjhoarSC8qP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #919.
Every visit of the admin panel live map page (
/gameServer/{id}/map/{mapId}) — and every terrain image request — permanently leaked oneGameMapInfoAdapterper hosted map, becauseObservableGameServerAdapternever disposed them. The leaked adapters stayed subscribed to theObjectAdded/ObjectRemovedevents of the liveGameMaps, so they were rooted forever and additionally slowed down the game loop: those events are awaited sequentially on every spawn, drop, despawn and map transition.Changes
The leak itself
ObservableGameServerAdapter.Dispose(bool)now unsubscribes and disposes itsIGameMapInfos and clears the list. That's the actual fix —MapPageandGameServeralready disposed the adapter, but the disposal didn't reach the map adapters.OnGameMapCreatedadditionally disposes a map adapter which was created concurrently to the disposal, instead of adding it to the already cleared list.Avoiding the expensive adapter creation altogether
InitializeAsyncgot awithMapInfosparameter (defaulttrue, so the Dapr singleton registration is unaffected). Callers which only need to register a map observer can skip creating and observing an adapter for every map on the server.MapPageonly needed the map infos to show the map name in the breadcrumb. It now initializes the adapter without them and resolves the name directly at theIGameServerContext.TerrainControllerdoesn't create anObservableGameServerAdapterat all anymore. It looks theGameMapup at the context and renders the terrain through a newGetTerrainStream(this GameMap)overload. This resolves the existing// TODO: Do this without creating an ObservableGameServerAdapter, because that's a very expensive operation.The PNG cache is shared between both overloads, so nothing is rendered twice.Component defects found in the same code
MapPlayerListsubscribed toIMapController.PlayersChangedinOnParametersSet— which runs on every re-render of the parentMapcomponent — and never unsubscribed. Handlers piled up, so after n parent renders a single player entering scope triggered n re-renders of the whole player table. It now subscribes only when the controller instance actually changed, keeps the handler in a field and unsubscribes inDispose.Mapcomponent: removed the synchronousDispose.Task.Run(this.DisposeAsync).Wait()bound toTask.Run(Func<TResult>)and produced aTask<ValueTask>, so the innerValueTaskwas never awaited — and it would have blocked the renderer thread. Blazor prefersIAsyncDisposablewhen a component implements both, so the remainingDisposeAsyncis the path that was already taken.MapController.DisposeAsyncnow awaitsObserverToWorldViewAdapter.DisposeAsync()instead of calling the synchronous no-opDispose(), so itsDisposeAsyncCore(clearing_observingObjectsand asserting thatObservingBucketsis empty) actually runs.Testing
dotnet build src/MUnique.OpenMU.sln -p:ci=true— succeeds, no new warnings in the touched files.dotnet test tests/MUnique.OpenMU.Web.Tests— 88 passed, 0 failed.🤖 Generated with Claude Code
https://claude.ai/code/session_01XyXbCxbtDecRjhoarSC8qP
Generated by Claude Code