Skip to content

Commit de2d241

Browse files
committed
Shuttle interface refactor: lazy loading only designs that are actually interacted with, to save on memory
1 parent 20d3738 commit de2d241

File tree

1 file changed

+160
-39
lines changed

1 file changed

+160
-39
lines changed

src/ttboard/project_mux.py

Lines changed: 160 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,110 +7,221 @@
77

88
import json
99
import re
10+
import gc
1011
import ttboard.util.time as time
1112
from ttboard.pins.pins import Pins
1213
from ttboard.boot.rom import ChipROM
1314
from ttboard.boot.shuttle_properties import HardcodedShuttle
1415
import ttboard.logging as logging
1516
log = logging.getLogger(__name__)
1617

18+
1719
'''
1820
Fetched with
1921
https://index.tinytapeout.com/tt03p5.json?fields=repo,address,commit,clock_hz,title
2022
https://index.tinytapeout.com/tt04.json?fields=repo,address,commit,clock_hz,title
2123
2224
'''
2325
class Design:
24-
BadCharsRe = re.compile(r'[^\w\d\s]+')
25-
SpaceCharsRe = re.compile(r'\s+')
26-
def __init__(self, projectMux, projindex:int, info:dict):
26+
def __init__(self, projectMux, projname:str, projindex:int, info:dict):
2727
self.mux = projectMux
28-
self.project_index = projindex
2928
self.count = int(projindex)
29+
self.name = projname
3030
self.macro = info['macro']
31-
self.name = info['macro']
32-
self.repo = info['repo']
33-
self.commit = info['commit']
31+
self.repo = ''
32+
if 'repo' in info:
33+
self.repo = info['repo']
34+
35+
self.commit = ''
36+
if 'commit' in info:
37+
self.commit = info['commit']
3438
self.clock_hz = info['clock_hz']
35-
# special cleanup for wokwi gen'ed names
36-
if self.name.startswith('tt_um_wokwi') and 'title' in info and len(info['title']):
37-
new_name = self.SpaceCharsRe.sub('_', self.BadCharsRe.sub('', info['title'])).lower()
38-
if len(new_name):
39-
self.name = f'wokwi_{new_name}'
40-
4139
self._all = info
4240

41+
@property
42+
def project_index(self):
43+
return self.count
44+
4345
def enable(self):
4446
self.mux.enable(self)
4547

4648
def disable(self):
4749
self.mux.disable()
4850

4951
def __str__(self):
50-
return f'{self.name} ({self.project_index}) @ {self.repo}'
52+
return f'{self.name} ({self.count}) @ {self.repo}'
5153

5254
def __repr__(self):
53-
return f'<Design {self.project_index}: {self.name}>'
55+
return f'<Design {self.count}: {self.name}>'
5456

57+
58+
class DesignStub:
59+
'''
60+
A yet-to-be-loaded design, just a pointer that will
61+
auto-load the design if accessed.
62+
Has a side effect of replacing itself as an attribute
63+
in the design index so this only happens once.
64+
'''
65+
def __init__(self, design_index, projname):
66+
self.design_index = design_index
67+
self.name = projname
68+
self._des = None
69+
70+
def _lazy_load(self):
71+
des = self.design_index.load_project(self.name)
72+
setattr(self.design_index, self.name, des)
73+
self._des = des
74+
return des
75+
76+
def __getattr__(self, name:str):
77+
if hasattr(self, '_des') and self._des is not None:
78+
des = self._des
79+
else:
80+
des = self._lazy_load()
81+
return getattr(des, name)
82+
83+
def __dir__(self):
84+
des = self._lazy_load()
85+
return dir(des)
86+
87+
def __repr__(self):
88+
return f'<Design {self.name} (uninit)>'
89+
5590
class DesignIndex:
91+
92+
BadCharsRe = re.compile(r'[^\w\d\s]+')
93+
SpaceCharsRe = re.compile(r'\s+')
94+
5695
def __init__(self, projectMux, src_JSON_file:str='shuttle_index.json'):
57-
self._shuttle_index = dict()
96+
self._src_json = src_JSON_file
97+
self._project_mux = projectMux
5898
self._project_count = 0
99+
self.load_available(src_JSON_file)
100+
101+
def load_available(self, src_JSON_file:str=None):
102+
if src_JSON_file is None:
103+
src_JSON_file = self._src_json
104+
105+
self._shuttle_index = dict()
106+
self._available_projects = dict()
59107
try:
60108
with open(src_JSON_file) as fh:
61109
index = json.load(fh)
62-
for project in index["projects"]:
63-
des = Design(projectMux, project["address"], project)
64-
attrib_name = des.name
65-
if attrib_name in self._shuttle_index:
110+
for project in index['projects']:
111+
attrib_name = project['macro']
112+
project_address = int(project['address'])
113+
114+
if attrib_name in self._available_projects:
66115
log.info(f'Already have a "{attrib_name}" here...')
67116
attempt = 1
68117
augmented_name = f'{attrib_name}_{attempt}'
69-
while augmented_name in self._shuttle_index:
118+
while augmented_name in self._available_projects:
70119
attempt += 1
71120
augmented_name = f'{attrib_name}_{attempt}'
72121

73122
attrib_name = augmented_name
74-
des.name = augmented_name
75-
self._shuttle_index[attrib_name] = des
76-
setattr(self, attrib_name, des)
123+
124+
attrib_name = self._wokwi_name_cleanup(attrib_name, project)
125+
self._available_projects[attrib_name] = int(project_address)
126+
setattr(self, attrib_name, DesignStub(self, attrib_name))
77127
self._project_count += 1
128+
78129
except OSError:
79130
log.error(f'Could not open shuttle index {src_JSON_file}')
131+
132+
gc.collect()
133+
80134

81135

136+
137+
def _wokwi_name_cleanup(self, name:str, info:dict):
138+
139+
# special cleanup for wokwi gen'ed names
140+
if name.startswith('tt_um_wokwi') and 'title' in info and len(info['title']):
141+
new_name = self.SpaceCharsRe.sub('_', self.BadCharsRe.sub('', info['title'])).lower()
142+
if len(new_name):
143+
name = f'wokwi_{new_name}'
144+
145+
return name
82146
@property
83147
def count(self):
84148
return self._project_count
85149

86150
@property
87151
def names(self):
88-
return sorted(self._shuttle_index.keys())
89-
152+
return sorted(self._available_projects.keys())
90153
@property
91154
def all(self):
155+
'''
156+
all available projects in the shuttle, whether loaded or not
157+
'''
158+
return list(map(lambda p: getattr(self, p), sorted(self._available_projects.keys())))
159+
@property
160+
def all_loaded(self):
161+
'''
162+
all the projects that have been lazy-loaded, basically
163+
anything you've actually enabled or accessed in any way.
164+
'''
92165
return sorted(self._shuttle_index.values(), key=lambda p: p.name)
93166

167+
94168
def get(self, project_name:str) -> Design:
169+
if not self.is_available(project_name):
170+
# not in list of available, maybe it's an integer?
171+
try:
172+
des_idx = int(project_name)
173+
for des in self._available_projects.items():
174+
if des[1] == des_idx:
175+
return self.get(des[0])
176+
except ValueError:
177+
pass
178+
raise AttributeError(f'Unknown project "{project_name}"')
179+
180+
if hasattr(self, project_name):
181+
return getattr(self, project_name)
182+
95183
if project_name in self._shuttle_index:
96184
return self._shuttle_index[project_name]
97185

98-
# maybe it's an integer?
99-
try:
100-
des_idx = int(project_name)
101-
for des in self.all:
102-
if des.count == des_idx:
103-
return des
104-
except ValueError:
105-
pass
186+
return self.load_project(project_name)
187+
188+
def load_project(self, project_name:str) -> Design:
189+
190+
# neither a know integer nor a loaded project, but is avail
191+
project_address = self._available_projects[project_name]
192+
try:
193+
with open(self._src_json) as fh:
194+
index = json.load(fh)
195+
for project in index['projects']:
196+
if int(project['address']) == project_address:
197+
# this is our guy
198+
des = Design(self._project_mux, project_name, project["address"], project)
199+
self._shuttle_index[des.name] = des
200+
gc.collect()
201+
return des
202+
203+
204+
except OSError:
205+
log.error(f'Could not open shuttle index {self._src_json}')
106206

107-
raise ValueError(f'Unknown project "{project_name}"')
207+
raise AttributeError(f'Unknown project "{project_name}"')
108208

209+
def is_available(self, project_name:str):
210+
return project_name in self._available_projects
211+
109212
def __len__(self):
110-
return len(self._shuttle_index)
213+
return len(self._available_projects)
214+
def __getattr__(self, name:str):
215+
if hasattr(self, '_shuttle_index') and name in self._shuttle_index:
216+
return self._shuttle_index[name]
217+
218+
return self.get(name)
111219

112220
def __getitem__(self, idx:int) -> Design:
113221
return self.get(idx)
222+
223+
def __dir__(self):
224+
return list(self._available_projects.keys())
114225

115226
def __repr__(self):
116227
return f'<DesignIndex {len(self)} projects>'
@@ -248,18 +359,28 @@ def find(self, search:str) -> list:
248359
return list(filter(lambda p: p.name.find(search) >= 0, self.all))
249360

250361
def __getattr__(self, name):
251-
if hasattr(self, 'projects') and hasattr(self.projects, name):
252-
return getattr(self.projects, name)
362+
if hasattr(self, 'projects'):
363+
if self.projects.is_available(name) or hasattr(self.projects, name):
364+
return getattr(self.projects, name)
253365
raise AttributeError(f"What is '{name}'?")
254366

255367
def __getitem__(self, key) -> Design:
256368
if hasattr(self, 'projects'):
257369
return self.projects[key]
258370
raise None
259371

372+
def __dir__(self):
373+
# this doesn't seem to do what I want in uPython?
374+
if hasattr(self, 'projects'):
375+
return self.projects.names
376+
return []
377+
378+
379+
def __len__(self):
380+
return len(self.projects)
260381

261382
def __str__(self):
262-
return f'Shuttle {self.run}\n{self.all}'
383+
return f'Shuttle {self.run}'
263384

264385
def __repr__(self):
265386
des_idx = self.projects

0 commit comments

Comments
 (0)