Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Nethermind/Nethermind.Core/Collections/TypeAsKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;

namespace Nethermind.Core.Collections;

public readonly struct TypeAsKey(Type key) : IEquatable<TypeAsKey>
{
private readonly Type _key = key;

public static implicit operator Type(TypeAsKey key) => key._key;
public static implicit operator TypeAsKey(Type key) => new(key);

public bool Equals(TypeAsKey other) => ReferenceEquals(_key, other._key);
public override int GetHashCode() => _key?.GetHashCode() ?? 0;
public override bool Equals(object? obj) => obj is TypeAsKey && Equals((TypeAsKey)obj);
}
43 changes: 42 additions & 1 deletion src/Nethermind/Nethermind.JsonRpc/JsonRpcService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Nethermind.Core;
using Nethermind.Core.Collections;
using Nethermind.Core.Threading;
using Nethermind.JsonRpc.Exceptions;
using Nethermind.JsonRpc.Modules;
Expand All @@ -27,8 +29,11 @@ public class JsonRpcService : IJsonRpcService
private readonly ILogger _logger;
private readonly IRpcModuleProvider _rpcModuleProvider;
private readonly HashSet<string> _methodsLoggingFiltering;
private readonly Lock _propertyInfoModificationLock = new();
private readonly int _maxLoggedRequestParametersCharacters;

private Dictionary<TypeAsKey, PropertyInfo?> _propertyInfoCache = [];

public JsonRpcService(IRpcModuleProvider rpcModuleProvider, ILogManager logManager, IJsonRpcConfig jsonRpcConfig)
{
_logger = logManager.GetClassLogger<JsonRpcService>();
Expand Down Expand Up @@ -196,7 +201,7 @@ private async Task<JsonRpcResponse> ExecuteAsync(JsonRpcRequest request, string
break;
case Task task:
await task;
resultWrapper = task.GetType().GetProperty("Result")?.GetValue(task) as IResultWrapper;
resultWrapper = GetResultProperty(task)?.GetValue(task) as IResultWrapper;
break;
}
}
Expand Down Expand Up @@ -244,6 +249,42 @@ private async Task<JsonRpcResponse> ExecuteAsync(JsonRpcRequest request, string
: GetSuccessResponse(methodName, resultWrapper.Data, request.Id, returnAction);
}

private PropertyInfo? GetResultProperty(Task task)
{
Type type = task.GetType();
if (_propertyInfoCache.TryGetValue(type, out PropertyInfo? value))
{
return value;
}

return GetResultPropertySlow(type);
}

private PropertyInfo? GetResultPropertySlow(Type type)
{
lock (_propertyInfoModificationLock)
{
Dictionary<TypeAsKey, PropertyInfo?> current = _propertyInfoCache;
// Re-check inside the lock in case another thread already added it
if (current.TryGetValue(type, out PropertyInfo? value))
{
return value;
}

// Copy-on-write: create a new dictionary so we don't mutate
// the one other threads may be reading without locks.
Dictionary<TypeAsKey, PropertyInfo?> propertyInfoCache = new(current);
PropertyInfo? propertyInfo = type.GetProperty("Result");
propertyInfoCache[type] = propertyInfo;
Comment on lines +276 to +278
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe do frozendictionary then?


// Publish the new cache instance atomically by swapping the reference.
// Readers grabbing _propertyInfoCache will now see the updated dictionary.
_propertyInfoCache = propertyInfoCache;

return propertyInfo;
}
}

private void LogRequest(string methodName, JsonElement providedParameters, ExpectedParameter[] expectedParameters)
{
if (_logger.IsTrace && !_methodsLoggingFiltering.Contains(methodName))
Expand Down