Skip to content

Commit 8a90577

Browse files
committed
More performant attrdict implementation:
Speeds up the attrdict middleware by nearly 2x. 5000 calls with a large block static response, removing any latency from the server: - ~3.5 seconds w/ no middleware - ~7.2 seconds w/ attrdict middleware on this branch (3.7s more) - ~10.3 seconds w/ attrdict middleware on main (6.8s more) 6.8/3.7 = 1.84x Changes: - Simplify the recursive method of ``AttributeDict`` by removing the dependence on ``recursive_map`` and using a simpler recursive approach to nested lists and dicts. - Don't use ``assoc`` as this ends up creating a new dict every time, adding more overhead.
1 parent 5581c5f commit 8a90577

File tree

3 files changed

+24
-38
lines changed

3 files changed

+24
-38
lines changed

newsfragments/3575.performance.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve ``AttributeDict.recursive()`` and ``AttributeDictMiddleware`` performance, effectively speeding up response processing for attrdict middleware by nearly 2x.

web3/datastructures.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
Optional,
1616
Sequence,
1717
Tuple,
18-
Type,
1918
TypeVar,
2019
Union,
2120
cast,
@@ -25,9 +24,6 @@
2524
is_integer,
2625
)
2726

28-
from web3._utils.formatters import (
29-
recursive_map,
30-
)
3127
from web3.exceptions import (
3228
Web3AssertionError,
3329
Web3TypeError,
@@ -81,19 +77,18 @@ def _repr_pretty_(self, builder: Any, cycle: bool) -> None:
8177
builder.text(")")
8278

8379
@classmethod
84-
def _apply_if_mapping(cls: Type[T], value: TValue) -> Union[T, TValue]:
80+
def recursive(cls, value: TValue) -> Any:
81+
"""
82+
Recursively convert mappings to ReadableAttributeDict instances and
83+
process nested collections (e.g., lists, sets, and dictionaries).
84+
"""
8585
if isinstance(value, Mapping):
86-
# error: Too many arguments for "object"
87-
return cls(value) # type: ignore
88-
else:
89-
return value
90-
91-
@classmethod
92-
def recursive(cls, value: TValue) -> "ReadableAttributeDict[TKey, TValue]":
93-
return cast(
94-
"ReadableAttributeDict[TKey, TValue]",
95-
recursive_map(cls._apply_if_mapping, value),
96-
)
86+
return cls({k: cls.recursive(v) for k, v in value.items()})
87+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
88+
return type(value)([cls.recursive(v) for v in value]) # type: ignore
89+
elif isinstance(value, set):
90+
return {cls.recursive(v) for v in value}
91+
return value
9792

9893

9994
class MutableAttributeDict(

web3/middleware/attrdict.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
cast,
88
)
99

10-
from eth_utils.toolz import (
11-
assoc,
12-
)
13-
1410
from web3.datastructures import (
1511
AttributeDict,
1612
)
@@ -33,21 +29,18 @@
3329

3430

3531
def _handle_async_response(response: "RPCResponse") -> "RPCResponse":
32+
"""
33+
Process the RPC response by converting nested dictionaries into AttributeDict.
34+
"""
3635
if "result" in response:
37-
return assoc(response, "result", AttributeDict.recursive(response["result"]))
36+
response["result"] = AttributeDict.recursive(response["result"])
3837
elif "params" in response and "result" in response["params"]:
39-
# this is a subscription response
40-
return assoc(
41-
response,
42-
"params",
43-
assoc(
44-
response["params"],
45-
"result",
46-
AttributeDict.recursive(response["params"]["result"]),
47-
),
38+
# subscription response
39+
response["params"]["result"] = AttributeDict.recursive(
40+
response["params"]["result"]
4841
)
49-
else:
50-
return response
42+
43+
return response
5144

5245

5346
class AttributeDictMiddleware(Web3Middleware, ABC):
@@ -60,19 +53,16 @@ class AttributeDictMiddleware(Web3Middleware, ABC):
6053

6154
def response_processor(self, method: "RPCEndpoint", response: "RPCResponse") -> Any:
6255
if "result" in response:
63-
return assoc(
64-
response, "result", AttributeDict.recursive(response["result"])
65-
)
66-
else:
67-
return response
56+
new_result = AttributeDict.recursive(response["result"])
57+
response = {**response, "result": new_result}
58+
return response
6859

6960
# -- async -- #
7061

7162
async def async_response_processor(
7263
self, method: "RPCEndpoint", response: "RPCResponse"
7364
) -> Any:
7465
if self._w3.provider.has_persistent_connection:
75-
# asynchronous response processing
7666
provider = cast("PersistentConnectionProvider", self._w3.provider)
7767
provider._request_processor.append_middleware_response_processor(
7868
response, _handle_async_response

0 commit comments

Comments
 (0)