Finding: PERF-17 (Low) + PERF-18 (Low) · Area: F1Server.Service / F1Server.Db
Source: docs/concepts/udp-packet-processing-performance-review.md (branch claude/udp-packet-processing-review-798u7o), section 2.3
Files involved
F1Server.Service/Processors/ProcessorFactory.cs:113-375 — nine near-identical Get{X}Processor methods, each doing ContainsKey + TryGetValue (double hash lookup per packet) and creating via Activator.CreateInstance
F1Server.Db/Entity/RepositoryFactory.cs:92-102 — GetRepository<T> constructs repositories via Activator.CreateInstance
F1Server.Db/Entity/Repositories/Base/RepositoryBase{TQueryable,TEntity}.cs:65-68 — GetQuery() uses Activator.CreateInstance per query
Problem / Impact
Small but constant reflection and double-lookup overhead multiplied by per-packet/per-query call frequency, plus ~250 lines of copy-paste in ProcessorFactory.
Step-by-step fix
- ProcessorFactory: replace the nine
Get{X}Processor methods with one private generic helper:
private TProcessor GetOrCreateProcessor<TProcessor>(Func<TProcessor> factory)
where TProcessor : BaseProcessor
{
return (TProcessor)_processors.GetOrAdd(typeof(TProcessor), _ => factory());
}
and call it from the switch in GetProcessor with direct constructors, e.g. PacketTypes.Session => GetOrCreateProcessor(() => new SessionProcessor(_serviceProvider, packetHeader, gameInfo)). Behavior note: RemoveAllProcessors() on session change and the frame-identifier logic in GetProcessor stay exactly as they are.
- RepositoryFactory.GetRepository: cache compiled constructor delegates in a
private static readonly ConcurrentDictionary<Type, Func<F1ServerDbContext, RepositoryBase>>, built once per repository type via Expression.New + Expression.Lambda. The per-instance _repositories dictionary stays (same semantics), only the creation path changes.
- GetQuery(): same approach — a static compiled delegate per
TQueryable type (Func<IQueryable<TEntity>, TQueryable>), replacing Activator.CreateInstance(typeof(TQueryable), ...).
- Note: all affected classes are
internal/public with known constructor signatures — if a constructor signature does not match at delegate-build time, fail fast with a clear exception message.
Test guidance
Existing processor and repository tests cover the behavior; they must stay green unchanged. Add one test asserting ProcessorFactory.GetProcessor returns the same instance for two packets of the same type and session, and a new instance after a session-id change (this pins the caching semantics the refactor must preserve).
Acceptance criteria
Related: #420 (long-lived factories reduce how often the repository delegates run — still worth removing the reflection).
Finding: PERF-17 (Low) + PERF-18 (Low) · Area: F1Server.Service / F1Server.Db
Source:
docs/concepts/udp-packet-processing-performance-review.md(branchclaude/udp-packet-processing-review-798u7o), section 2.3Files involved
F1Server.Service/Processors/ProcessorFactory.cs:113-375— nine near-identicalGet{X}Processormethods, each doingContainsKey+TryGetValue(double hash lookup per packet) and creating viaActivator.CreateInstanceF1Server.Db/Entity/RepositoryFactory.cs:92-102—GetRepository<T>constructs repositories viaActivator.CreateInstanceF1Server.Db/Entity/Repositories/Base/RepositoryBase{TQueryable,TEntity}.cs:65-68—GetQuery()usesActivator.CreateInstanceper queryProblem / Impact
Small but constant reflection and double-lookup overhead multiplied by per-packet/per-query call frequency, plus ~250 lines of copy-paste in
ProcessorFactory.Step-by-step fix
Get{X}Processormethods with one private generic helper:switchinGetProcessorwith direct constructors, e.g.PacketTypes.Session => GetOrCreateProcessor(() => new SessionProcessor(_serviceProvider, packetHeader, gameInfo)). Behavior note:RemoveAllProcessors()on session change and the frame-identifier logic inGetProcessorstay exactly as they are.private static readonly ConcurrentDictionary<Type, Func<F1ServerDbContext, RepositoryBase>>, built once per repository type viaExpression.New+Expression.Lambda. The per-instance_repositoriesdictionary stays (same semantics), only the creation path changes.TQueryabletype (Func<IQueryable<TEntity>, TQueryable>), replacingActivator.CreateInstance(typeof(TQueryable), ...).internal/publicwith known constructor signatures — if a constructor signature does not match at delegate-build time, fail fast with a clear exception message.Test guidance
Existing processor and repository tests cover the behavior; they must stay green unchanged. Add one test asserting
ProcessorFactory.GetProcessorreturns the same instance for two packets of the same type and session, and a new instance after a session-id change (this pins the caching semantics the refactor must preserve).Acceptance criteria
Activator.CreateInstancein the per-packet or per-query pathProcessorFactorycaching implemented once (generic helper), nine copies removedRH####) warningsRelated: #420 (long-lived factories reduce how often the repository delegates run — still worth removing the reflection).