2
$\begingroup$

In the conversation in comments to Driver based on shading types/ change boolean when switching shading types an argument has been made that the draw callback is called very frequently, and therefore slower than Msgbus.

Not investigating yet if it's actually slower (I guess it indeed is), I'm wondering how slow the draw callback is exactly; clearly, using one or a few is insignificant to the performance of typical hardware with teraflops of processing throughput. But stack enough add-ons or scripts and maybe you will start to feel it?

$\endgroup$
8
  • $\begingroup$ The average user doesn't have teraflops of performance for their _Python+ code and it's not flops that matters here, anyway. The cost of an individual callback invocation isn't the critical metric in measuring performance impact either. $\endgroup$ Commented Jan 28, 2022 at 20:26
  • 1
    $\begingroup$ @MartyFouts and what is? If you argue against using the technique, saying it's slower, what in it is slower exactly? You can divide the code into 3 parts: 1. setup, 2. interface, 3. Shared part. If the setup (subscribing to msgbus vs draw callbacks) is irrelevant, and the interface (msgbus callback vs draw callback) is irrelevant, and the shared part has the same performance, then I'm missing what's the point. $\endgroup$ Commented Jan 28, 2022 at 20:50
  • $\begingroup$ If you assume that the cost of a call to a msgbus handler is the same as the cost of a call to an app handler, then the difference is in the frequency of calls. msgbus handler gets called the minimum possible number of times: when the interesting property changes. app handler, in this instance, is being called for a different reason, so there are more calls. But the relative cost isn't identical, because msgbus handler doesn't need the check to see if something changes; and if statements are expensive in i-cache machines. $\endgroup$ Commented Jan 28, 2022 at 20:56
  • $\begingroup$ so in the end, you get a more expensive call, due to the if statement, made more frequently, due to why the calls are made. The sum of the product of those two things will always be larger than the other cost unless you can make the case that msgbus handler is >> app handler to call. $\endgroup$ Commented Jan 28, 2022 at 20:57
  • $\begingroup$ but even then, you have to show that the frequency of msgbus handler calls is enough higher than the frequency of app handler calls. . . $\endgroup$ Commented Jan 28, 2022 at 20:58

1 Answer 1

2
$\begingroup$

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:

$\endgroup$
7
  • $\begingroup$ The problem with benchmarks is that you have to know what you want to measure. You've not made a case that the cost of validating whether location has changed is indicative of the cost of validating other possible change values. The argument that "it would be choppy with msgbus" is based on the assumption that there would be print statements, which isn't the case and probably shouldn't be the case in your measurements. To measure callback overhead you should probably be using a profiler anyway. $\endgroup$ Commented Jan 28, 2022 at 20:31
  • $\begingroup$ @MartyFouts I don't understand your points, honestly. The test case proves beyond doubt the overhead of the method is insignificant. Why use a profiler, then? To double-prove it? To see what part of the total insignificant time is the most insignificant, and what part of the total insignificant time is the least significant? If anything, the answer is already overly pedantic. $\endgroup$ Commented Jan 28, 2022 at 20:58
  • $\begingroup$ what's the cost of the if statement, in the more common case when it tests false? $\endgroup$ Commented Jan 28, 2022 at 20:59
  • $\begingroup$ @MartyFouts the cost is insignificant, as the test shows. $\endgroup$ Commented Jan 28, 2022 at 21:04
  • $\begingroup$ insignificant with the wrong clause in the if statement and using an example that may not mimic the behavior of the actual user and still higher than the cost of not making the calls. It's not a useful argument against using msgbus. $\endgroup$ Commented Jan 28, 2022 at 21:06

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .