Skip to content

Commit 33bb0bd

Browse files
committed
Cache device assignments/attachments
Those are enumerated several times when listing devices, but they change rarely (most qubes don't get any devices attached usually). Cache both assignments and attachments to speed things up. And similarly to properties and power state cache - invalidate the cache based on events. Fixes QubesOS/qubes-issues#10380
1 parent f9eb4a5 commit 33bb0bd

File tree

4 files changed

+108
-14
lines changed

4 files changed

+108
-14
lines changed

qubesadmin/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,8 +797,10 @@ def _invalidate_cache_all(self):
797797
# pylint: disable=protected-access
798798
self.domains.clear_cache()
799799
for vm in self.domains._vm_objects.values():
800+
assert isinstance(vm, qubesadmin.vm.QubesVM)
800801
vm._power_state_cache = None
801802
vm._properties_cache = {}
803+
vm.devices.clear_cache()
802804
self._properties_cache = {}
803805

804806

qubesadmin/devices.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ def __init__(self, vm, class_):
6262
self._vm = vm
6363
self._class = class_
6464
self._dev_cache = {}
65+
#: attachments cache, `None` means "not cached (yet)",
66+
#: in contrast to empty list which means "cached empty list"
67+
self._attachment_cache = None
68+
#: assignments cache, `None` means "not cached (yet)",
69+
#: in contrast to empty list which means "cached empty list"
70+
self._assignment_cache = None
6571

6672
def attach(self, assignment: DeviceAssignment) -> None:
6773
"""
@@ -75,6 +81,9 @@ def attach(self, assignment: DeviceAssignment) -> None:
7581
"did you mean `qvm-pci assign --required ...`"
7682
)
7783
self._add(assignment, "attach")
84+
# clear the whole cache instead of saving provided assignment, it might
85+
# get modified before actually attaching
86+
self._attachment_cache = None
7887

7988
def detach(self, assignment: DeviceAssignment) -> None:
8089
"""
@@ -84,6 +93,7 @@ def detach(self, assignment: DeviceAssignment) -> None:
8493
(obtained from :py:meth:`assignments`)
8594
"""
8695
self._remove(assignment, "detach")
96+
self._assignment_cache = None
8797

8898
def assign(self, assignment: DeviceAssignment) -> None:
8999
"""
@@ -113,6 +123,9 @@ def assign(self, assignment: DeviceAssignment) -> None:
113123
)
114124

115125
self._add(assignment, "assign")
126+
# clear the whole cache instead of saving provided assignment, it might
127+
# get modified before actually assigning
128+
self._assignment_cache = None
116129

117130
def unassign(self, assignment: DeviceAssignment) -> None:
118131
"""
@@ -122,6 +135,7 @@ def unassign(self, assignment: DeviceAssignment) -> None:
122135
(obtained from :py:meth:`assignments`)
123136
"""
124137
self._remove(assignment, "unassign")
138+
self._assignment_cache = None
125139

126140
def _add(self, assignment: DeviceAssignment, action: str) -> None:
127141
"""
@@ -186,6 +200,10 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]:
186200
"""
187201
List devices which are attached to this vm.
188202
"""
203+
if self._attachment_cache is not None:
204+
yield from self._attachment_cache
205+
return
206+
new_cache = []
189207
assignments_str = self._vm.qubesd_call(
190208
None, "admin.vm.device.{}.Attached".format(self._class)
191209
).decode()
@@ -195,9 +213,14 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]:
195213
head, self._class, self._vm.app.domains, blind=True
196214
)
197215

198-
yield DeviceAssignment.deserialize(
216+
assignment = DeviceAssignment.deserialize(
199217
untrusted_rest.encode("ascii"), expected_device=device
200218
)
219+
new_cache.append(assignment)
220+
yield assignment
221+
222+
if self._vm.app.cache_enabled:
223+
self._attachment_cache = new_cache
201224

202225
def get_assigned_devices(
203226
self, required_only: bool = False
@@ -207,6 +230,12 @@ def get_assigned_devices(
207230
208231
Safe to access before libvirt bootstrap.
209232
"""
233+
if self._assignment_cache is not None:
234+
for assignment in self._assignment_cache:
235+
if not required_only or assignment.required:
236+
yield assignment
237+
return
238+
new_cache = []
210239
assignments_str = self._vm.qubesd_call(
211240
None, "admin.vm.device.{}.Assigned".format(self._class)
212241
).decode()
@@ -219,9 +248,13 @@ def get_assigned_devices(
219248
assignment = DeviceAssignment.deserialize(
220249
untrusted_rest.encode("ascii"), expected_device=device
221250
)
251+
new_cache.append(assignment)
222252
if not required_only or assignment.required:
223253
yield assignment
224254

255+
if self._vm.app.cache_enabled:
256+
self._assignment_cache = new_cache
257+
225258
def get_exposed_devices(self) -> Iterable[DeviceInfo]:
226259
"""
227260
List devices exposed by this vm.
@@ -252,6 +285,7 @@ def update_assignment(self, device: Port, required: AssignmentMode):
252285
repr(device),
253286
required.value.encode("utf-8"),
254287
)
288+
self._assignment_cache = None
255289

256290
__iter__ = get_exposed_devices
257291

@@ -260,6 +294,8 @@ def clear_cache(self):
260294
Clear cache of available devices.
261295
"""
262296
self._dev_cache.clear()
297+
self._assignment_cache = None
298+
self._attachment_cache = None
263299

264300
def __getitem__(self, item):
265301
"""Get device object with given port_id.
@@ -325,3 +361,8 @@ def allow(self, *interfaces: Iterable[DeviceInterface]):
325361
None,
326362
"".join(repr(ifc) for ifc in interfaces).encode('ascii'),
327363
)
364+
365+
def clear_cache(self):
366+
"""Clear cache of all available device classes"""
367+
for devclass in self.values():
368+
devclass.clear_cache()

qubesadmin/events/__init__.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,27 @@ def handle(self, subject, event, **kwargs):
229229
vm = kwargs['vm']
230230
self.app.domains.clear_cache(invalidate_name=str(vm))
231231
subject = None
232+
# invalidate cache if needed; call it before other handlers
233+
# as those may want to use cached value
234+
if event.startswith('property-set:') or \
235+
event.startswith('property-reset:'):
236+
self.app._invalidate_cache(subject, event, **kwargs)
237+
elif event in ('domain-pre-start', 'domain-start', 'domain-shutdown',
238+
'domain-paused', 'domain-unpaused',
239+
'domain-start-failed'):
240+
self.app._update_power_state_cache(subject, event, **kwargs)
241+
subject.devices.clear_cache()
242+
elif event == 'connection-established':
243+
# on (re)connection, clear cache completely - we don't have
244+
# guarantee about not missing any events before this point
245+
self.app._invalidate_cache_all()
246+
elif event.split(":")[0] in ("device-assign", "device-unassign", "device-assignment-changed"):
247+
devclass = event.split(":")[1]
248+
subject.devices[devclass]._assignment_cache = None
249+
elif event.split(":")[0] in ("device-attach", "device-detach", "device-removed"):
250+
devclass = event.split(":")[1]
251+
subject.devices[devclass]._attachment_cache = None
252+
232253
# deserialize known attributes
233254
if event.startswith('device-'):
234255
try:
@@ -256,19 +277,6 @@ def handle(self, subject, event, **kwargs):
256277
kwargs['port'], devclass, self.app.domains, blind=True)
257278
except (KeyError, ValueError):
258279
pass
259-
# invalidate cache if needed; call it before other handlers
260-
# as those may want to use cached value
261-
if event.startswith('property-set:') or \
262-
event.startswith('property-reset:'):
263-
self.app._invalidate_cache(subject, event, **kwargs)
264-
elif event in ('domain-pre-start', 'domain-start', 'domain-shutdown',
265-
'domain-paused', 'domain-unpaused',
266-
'domain-start-failed'):
267-
self.app._update_power_state_cache(subject, event, **kwargs)
268-
elif event == 'connection-established':
269-
# on (re)connection, clear cache completely - we don't have
270-
# guarantee about not missing any events before this point
271-
self.app._invalidate_cache_all()
272280

273281
handlers = [h_func for h_name, h_func_set in self.handlers.items()
274282
for h_func in h_func_set

qubesadmin/tests/devices.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,49 @@ def test_041_assignments_options(self):
255255

256256
self.assertAllCalled()
257257

258+
def test_042_assignments_cache(self):
259+
self.app.cache_enabled = True
260+
self.app.expected_calls[
261+
('test-vm', 'admin.vm.device.test.Attached', None, None)] = \
262+
(b"0\0test-vm2+dev1 backend_domain='test-vm2' port_id='dev1' "
263+
b"mode='manual' devclass='test' "
264+
b"frontend_domain='test-vm' _ro='True'\n")
265+
self.app.expected_calls[
266+
('test-vm', 'admin.vm.device.test.Assigned', None, None)] = \
267+
(b"0\0test-vm3+dev2 backend_domain='test-vm3' devclass='test' "
268+
b"port_id='dev2' mode='required' "
269+
b"frontend_domain='test-vm' _ro='False'\n")
270+
# populate cache
271+
list(self.vm.devices['test'].get_dedicated_devices())
272+
273+
self.assertAllCalled()
274+
self.app.expected_calls.clear()
275+
276+
# get again, should be cached now
277+
assigns = sorted(list(
278+
self.vm.devices['test'].get_dedicated_devices()))
279+
280+
self.assertEqual(len(assigns), 2)
281+
self.assertIsInstance(assigns[0], DeviceAssignment)
282+
self.assertEqual(assigns[0].backend_domain,
283+
self.app.domains['test-vm2'])
284+
self.assertEqual(assigns[0].port_id, 'dev1')
285+
self.assertEqual(assigns[0].frontend_domain,
286+
self.app.domains['test-vm'])
287+
self.assertEqual(assigns[0].options, {'ro': 'True'})
288+
self.assertEqual(assigns[0].required, False)
289+
self.assertEqual(assigns[0].devclass, 'test')
290+
291+
self.assertIsInstance(assigns[1], DeviceAssignment)
292+
self.assertEqual(assigns[1].backend_domain,
293+
self.app.domains['test-vm3'])
294+
self.assertEqual(assigns[1].port_id, 'dev2')
295+
self.assertEqual(assigns[1].frontend_domain,
296+
self.app.domains['test-vm'])
297+
self.assertEqual(assigns[1].options, {'ro': 'False'})
298+
self.assertEqual(assigns[1].required, True)
299+
self.assertEqual(assigns[1].devclass, 'test')
300+
258301
def test_050_required(self):
259302
self.app.expected_calls[
260303
('test-vm', 'admin.vm.device.test.Assigned', None, None)] = \

0 commit comments

Comments
 (0)