Skip to content

Commit dec75c9

Browse files
authored
feat(fill): Improve mem usage by: fixture collector flush + lazy loading pre-alloc groups (#2032)
* feat(FixtureCollector): Add flush interval to the fixture collector * fix: typing * fix: typing * fix: unit test
1 parent 54003b0 commit dec75c9

File tree

5 files changed

+59
-23
lines changed

5 files changed

+59
-23
lines changed

src/cli/show_pre_alloc_group_stats.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def calculate_size_distribution(
102102

103103
def analyze_pre_alloc_folder(folder: Path, verbose: int = 0) -> Dict:
104104
"""Analyze pre-allocation folder and return statistics."""
105-
pre_alloc_groups = PreAllocGroups.from_folder(folder)
105+
pre_alloc_groups = PreAllocGroups.from_folder(folder, lazy_load=False)
106106

107107
# Basic stats
108108
total_groups = len(pre_alloc_groups)
@@ -147,7 +147,7 @@ def analyze_pre_alloc_folder(folder: Path, verbose: int = 0) -> Dict:
147147

148148
# Calculate frequency distribution of group sizes
149149
group_distribution, test_distribution = calculate_size_distribution(
150-
[g["tests"] for g in group_details]
150+
[g["tests"] for g in group_details] # type: ignore
151151
)
152152

153153
# Analyze test functions split across multiple size-1 groups

src/ethereum_test_fixtures/collector.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class FixtureCollector:
113113
single_fixture_per_file: bool
114114
filler_path: Path
115115
base_dump_dir: Optional[Path] = None
116+
flush_interval: int = 1000
116117

117118
# Internal state
118119
all_fixtures: Dict[Path, Fixtures] = field(default_factory=dict)
@@ -151,6 +152,9 @@ def add_fixture(self, info: TestInfo, fixture: BaseFixture) -> Path:
151152

152153
self.all_fixtures[fixture_path][info.get_id()] = fixture
153154

155+
if self.flush_interval > 0 and len(self.all_fixtures) >= self.flush_interval:
156+
self.dump_fixtures()
157+
154158
return fixture_path
155159

156160
def dump_fixtures(self) -> None:
@@ -168,6 +172,8 @@ def dump_fixtures(self) -> None:
168172
raise TypeError("All fixtures in a single file must have the same format.")
169173
fixtures.collect_into_file(fixture_path)
170174

175+
self.all_fixtures.clear()
176+
171177
def verify_fixture_files(self, evm_fixture_verification: FixtureConsumer) -> None:
172178
"""Run `evm [state|block]test` on each fixture."""
173179
for fixture_path, name_fixture_dict in self.all_fixtures.items():

src/ethereum_test_fixtures/pre_alloc_groups.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import json
44
from pathlib import Path
5-
from typing import Any, Dict, List
5+
from typing import Any, Dict, Iterator, List, Tuple
66

77
from filelock import FileLock
8-
from pydantic import Field, computed_field
8+
from pydantic import Field, PrivateAttr, computed_field
99

1010
from ethereum_test_base_types import CamelModel, EthereumTestRootModel
1111
from ethereum_test_forks import Fork
@@ -84,35 +84,62 @@ def to_file(self, file: Path) -> None:
8484

8585

8686
class PreAllocGroups(EthereumTestRootModel):
87-
"""Root model mapping pre-allocation group hashes to test groups."""
87+
"""
88+
Root model mapping pre-allocation group hashes to test groups.
89+
90+
If lazy_load is True, the groups are not loaded from the folder until they are accessed.
91+
92+
Iterating will fail if lazy_load is True.
93+
"""
8894

89-
root: Dict[str, PreAllocGroup]
95+
root: Dict[str, PreAllocGroup | None]
96+
97+
_folder_source: Path | None = PrivateAttr(None)
9098

9199
def __setitem__(self, key: str, value: Any):
92100
"""Set item in root dict."""
101+
assert self._folder_source is None, (
102+
"Cannot set item in root dict after folder source is set"
103+
)
93104
self.root[key] = value
94105

95106
@classmethod
96-
def from_folder(cls, folder: Path) -> "PreAllocGroups":
107+
def from_folder(cls, folder: Path, *, lazy_load: bool = False) -> "PreAllocGroups":
97108
"""Create PreAllocGroups from a folder of pre-allocation files."""
98109
# First check for collision failures
99110
for fail_file in folder.glob("*.fail"):
100111
with open(fail_file) as f:
101112
raise Alloc.CollisionError.from_json(json.loads(f.read()))
102-
data = {}
113+
114+
data: Dict[str, PreAllocGroup | None] = {}
103115
for file in folder.glob("*.json"):
104-
with open(file) as f:
105-
data[file.stem] = PreAllocGroup.model_validate_json(f.read())
106-
return cls(root=data)
116+
if lazy_load:
117+
data[file.stem] = None
118+
else:
119+
with open(file) as f:
120+
data[file.stem] = PreAllocGroup.model_validate_json(f.read())
121+
instance = cls(root=data)
122+
if lazy_load:
123+
instance._folder_source = folder
124+
return instance
107125

108126
def to_folder(self, folder: Path) -> None:
109127
"""Save PreAllocGroups to a folder of pre-allocation files."""
110128
for key, value in self.root.items():
129+
assert value is not None, f"Value for key {key} is None"
111130
value.to_file(folder / f"{key}.json")
112131

113132
def __getitem__(self, item):
114133
"""Get item from root dict."""
115-
return self.root[item]
134+
if self._folder_source is None:
135+
item = self.root[item]
136+
assert item is not None, f"Item {item} is None"
137+
return item
138+
else:
139+
if self.root[item] is None:
140+
with open(self._folder_source / f"{item}.json") as f:
141+
self.root[item] = PreAllocGroup.model_validate_json(f.read())
142+
return self.root[item]
116143

117144
def __iter__(self):
118145
"""Iterate over root dict."""
@@ -130,10 +157,14 @@ def keys(self):
130157
"""Get keys from root dict."""
131158
return self.root.keys()
132159

133-
def values(self):
160+
def values(self) -> Iterator[PreAllocGroup]:
134161
"""Get values from root dict."""
135-
return self.root.values()
162+
for value in self.root.values():
163+
assert value is not None, "Value is None"
164+
yield value
136165

137-
def items(self):
166+
def items(self) -> Iterator[Tuple[str, PreAllocGroup]]:
138167
"""Get items from root dict."""
139-
return self.root.items()
168+
for key, value in self.root.items():
169+
assert value is not None, f"Value for key {key} is None"
170+
yield key, value

src/pytest_plugins/filler/filler.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def _load_pre_alloc_groups_from_folder(self) -> None:
226226
"""Load pre-allocation groups from the output folder."""
227227
pre_alloc_folder = self.fixture_output.pre_alloc_groups_folder_path
228228
if pre_alloc_folder.exists():
229-
self.pre_alloc_groups = PreAllocGroups.from_folder(pre_alloc_folder)
229+
self.pre_alloc_groups = PreAllocGroups.from_folder(pre_alloc_folder, lazy_load=True)
230230
else:
231231
raise FileNotFoundError(
232232
f"Pre-allocation groups folder not found: {pre_alloc_folder}. "
@@ -315,7 +315,7 @@ def aggregate_pre_alloc_groups(self, worker_groups: PreAllocGroups) -> None:
315315
if self.pre_alloc_groups is None:
316316
self.pre_alloc_groups = PreAllocGroups(root={})
317317

318-
for hash_key, group in worker_groups.root.items():
318+
for hash_key, group in worker_groups.items():
319319
if hash_key in self.pre_alloc_groups:
320320
# Merge if exists (should not happen in practice)
321321
existing = self.pre_alloc_groups[hash_key]
@@ -699,16 +699,15 @@ def pytest_terminal_summary(
699699
if config.pluginmanager.hasplugin("xdist"):
700700
# Load pre-allocation groups from disk
701701
pre_alloc_groups = PreAllocGroups.from_folder(
702-
config.fixture_output.pre_alloc_groups_folder_path # type: ignore[attr-defined]
702+
config.fixture_output.pre_alloc_groups_folder_path, # type: ignore[attr-defined]
703+
lazy_load=False,
703704
)
704705
else:
705706
assert session_instance.pre_alloc_groups is not None
706707
pre_alloc_groups = session_instance.pre_alloc_groups
707708

708709
total_groups = len(pre_alloc_groups.root)
709-
total_accounts = sum(
710-
group.pre_account_count for group in pre_alloc_groups.root.values()
711-
)
710+
total_accounts = sum(group.pre_account_count for group in pre_alloc_groups.values())
712711

713712
terminalreporter.write_sep(
714713
"=",

src/pytest_plugins/filler/tests/test_prealloc_group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def test_pre_alloc_grouping_by_test_type(
436436
Path(default_output_directory()).absolute() / "blockchain_tests_engine_x" / "pre_alloc"
437437
)
438438
assert output_dir.exists()
439-
groups = PreAllocGroups.from_folder(output_dir)
439+
groups = PreAllocGroups.from_folder(output_dir, lazy_load=False)
440440
if (
441441
len([f for f in output_dir.iterdir() if f.name.endswith(".json")])
442442
!= expected_different_pre_alloc_groups

0 commit comments

Comments
 (0)