Setup Code
Let's create a number (precised on line #6) of separate callbacks, one for each object, that is also created. The callbacks are very simple, they just print a location of an object which was moved - however, the complexity of the callback is not important, since it checks if a value has changed since the last check, before proceeding to the actual work. Therefore regardless of the number of unnecessary calls to the test
function, work done after return
statement will be done as many times as in case of the msgbus
callback (at least assuming the msgbus
works properly)
import bpy
from bpy import context as C, data as D
from bpy.types import SpaceView3D
from time import time
num_listeners = 100
last_values = {}
if not hasattr(bpy.types.SpaceView3D, 'my_handlers'):
bpy.types.SpaceView3D.my_handlers = []
def test(obname):
ob = D.objects[obname]
if ob.location.xyz == last_values[obname]:
return
print(f"{obname} changed location to {ob.location}")
last_values[obname] = ob.location.xyz
def register():
row_len = int(num_listeners**.5)
for i in range(num_listeners):
x, y = i % row_len, i // row_len
bpy.ops.mesh.primitive_cube_add(location=(x*2.1, y*2.1, 0))
C.object.name = str(i)
last_values[C.object.name] = C.object.location.xyz
# now add a separate handler to each object:
handler = SpaceView3D.draw_handler_add(test, (C.object.name,), 'WINDOW', 'PRE_VIEW')
SpaceView3D.my_handlers.append(handler)
def unregister():
for handler in SpaceView3D.my_handlers:
SpaceView3D.draw_handler_remove(handler, 'WINDOW')
bpy.types.SpaceView3D.my_handlers = []
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
last_values = {}
start = time()
unregister()
register()
print(time() - start)
Result
I measure the time at the end to see how long it takes:
- $1\over4$ of a second for 100 objects and callbacks
- $30$ seconds for 1000 objects and callbacks
- $29$ seconds for 1000 objects with callback registering commented out (lines #27-28)
- $2.4$ seconds for registering 6453 callbacks (without creating objects)
Even if that's a whole possible second added to Blender startup for 1000 callbacks, the scenario is unrealistic - You would need a thousand separate add-ons to use this technique. Otherwise, if a single add-on observes multiple things, it should pack the logic into a single callback function.
Aside from adding that many callbacks, how slow is it in the 3D viewport? Apparently, having 6453 of them on my PC makes Blender impractical to use - less than 1 FPS:
Next thing - I changed num_listeners to 0
and ran the script to clean everything. That took $40$ seconds.
Now a test for 1000 objects and callbacks:
The FPS isn't stable, but otherwise everything works smoothly. Moving too many objects is choppy because of hundreds of console prints multiple times per frame - it would be as choppy with msgbus.
100 callbacks, 100 objects each
This represents a more realistic scenario, where 100 add-ons register a single draw callback, but each of these add-ons repeats the "check for changes" boilerplate:
import bpy
from bpy import context as C, data as D
from bpy.types import SpaceView3D
from time import time
num_listeners = 100
num_objects = 100
last_values = []
if not hasattr(bpy.types.SpaceView3D, 'my_handlers'):
bpy.types.SpaceView3D.my_handlers = []
def test(last):
for obname in last:
ob = D.objects[obname]
if ob.location.xyz == last[obname]:
continue
if last in last_values[:5]:
# print only in the first five handlers:
print(f"{obname} changed location to {ob.location}")
last[obname] = ob.location.xyz
def register():
row_len = int(num_listeners**.5)
starting_values = {}
for i in range(num_listeners):
x, y = i % row_len, i // row_len
bpy.ops.mesh.primitive_cube_add(location=(x*2.1, y*2.1, 0))
C.object.name = str(i)
starting_values[C.object.name] = C.object.location.xyz
for i in range(num_objects):
# each handler supports all objects; yes, they share those objects
# but for the purposes of measuring performance of draw callback
# it doesn't matter. Passing a separate dictionary to each, so every
# handler maintains its own `last_values`
last_values.append(d:={k: v.copy for k, v in starting_values.items()})
handler = SpaceView3D.draw_handler_add(test, (d, ), 'WINDOW', 'PRE_VIEW')
SpaceView3D.my_handlers.append(handler)
def unregister():
for handler in SpaceView3D.my_handlers:
SpaceView3D.draw_handler_remove(handler, 'WINDOW')
bpy.types.SpaceView3D.my_handlers = []
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
last_values = []
start = time()
unregister()
register()
print(time() - start)
Despite 10 000 checks on each draw, there's no noticeable lag: