Performance Comparison

The table below compares the performance of maya.cmds, pymel, AL.omx, maya.api.OpenMaya (om2), when they are dealing with 100+, 1000+, 10000+ nodes. It also shows the performance difference between AL.omx in and out of immediate mode.

100+ nodes (seconds)

cmds

pymel

omx(immediate)

omx

om2

creation

0.1837

0.2977

0.0825

0.061

0.0746

edit

0.0677

0.3975

0.1692

0.0536

0.0270

rename

0.0115

0.0310

0.016

0.0055

0.0053

query

0.0082

0.1422

0.0146

0.0101

0.0057

delete

0.1156

0.1205

0.0133

0.0069

0.0059

total

0.3868

0.9890

0.2956

0.1371

0.1185

1000+ nodes (seconds)

cmds

pymel

omx(immediate)

omx

om2

creation

1.6022

2.8140

0.9777

0.6206

0.2677

edit

0.7045

4.1965

1.5021

0.4553

0.1740

rename

0.1246

0.3209

0.1138

0.0606

0.0434

query

0.0819

1.4160

0.1128

0.1231

0.0432

delete

0.8158

0.6460

0.1044

0.0777

0.0459

total

3.3291

9.3934

2.8108

1.3372

0.5742

10000+ nodes(seconds)

cmds

pymel

omx(immediate)

omx

om2

creation

15.7968

27.2677

9.2528

7.2431

2.4226

edit

7.0859

41.1648

13.5523

4.8801

1.7376

rename

1.4020

3.4139

1.1828

0.6959

0.5311

query

0.8260

14.0232

1.2039

1.275

0.4394

delete

6.2583

5.6137

1.6232

0.9046

0.5580

total

31.369

91.4833

26.815

14.9987

5.6887

The graph below shows the time taken to run each code snippet as the node count scales.

The time taken as the node count scales for each library.

Performance Measuring Code

Common utils used by each script, it needs to be executed first:

# execute these codes once before actual performance testing codes.

import random
import time


def getRandomPosition(furthestDist):
    return [(random.random() - 0.5) * furthestDist for _ in range(3)]


def getRandomScale():
    return [random.random() * 2.0 for _ in range(3)]


def getRandomColor():
    return random.randint(0, 31)


def getRandomIndex(maxValue):
    return random.randint(0, maxValue)


def getMaxValue(locCount):
    return int(locCount / 10)


class PerfMeasurement:
    def __init__(self, label):
        self._label = label
        self._gap = 0.0

    def __enter__(self,):
        self._start = time.time()
        self._gap = 0.0

    def __exit__(self, *_, **__):
        self._gap = time.time() - self._start
        print(f"{self._label} took {round(self._gap, 4)} seconds.")

    def timeConsumed(self):
        return self._gap


class TotalPerfMeasurement:
    def __init__(self, label):
        self._measurers = []
        self._label = label

    def add(self, label):
        measurement = PerfMeasurement(label)
        self._measurers.append(measurement)
        return measurement

    def __enter__(self,):
        print("-" * 20)
        return self

    def __exit__(self, *_, **__):
        total = 0.0
        for m in self._measurers:
            total = total + m.timeConsumed()

        print(f"{self._label} took {round(total, 4)} seconds.")
        print("-" * 20)


NUM_NODES_LIST = (100, 1000, 10000)
REFINED_NUM_NODES_LIST = (10, 50, 100, 500, 1000, 5000, 10000, 50000)

maya.cmds:

from maya import cmds

# execute the code from common in maya script editor first.


def createInCmds(locCount):
    maxValue = getMaxValue(locCount)

    controller = cmds.joint()
    cmds.addAttr(controller, ln="flash", at="long", min=0, max=maxValue)

    stars = [None] * locCount
    toDelete = [None] * locCount
    parent = cmds.createNode("transform", n="stars")

    for i in range(locCount):
        condition = cmds.createNode("condition")

        (loc,) = cmds.spaceLocator()
        cmds.parent(loc, parent)
        cmds.addAttr(loc, ln="flashIndex", at="long", min=0, max=maxValue)

        (testDel,) = cmds.spaceLocator()
        cmds.parent(testDel, loc)

        cmds.objExists(testDel)
        stars[i] = (loc, condition)
        toDelete[i] = testDel

    return controller, stars, toDelete


def editInCmds(controller, stars):
    maxValue = getMaxValue(len(stars))

    cmds.setAttr(f"{controller}.radius", 10)
    cmds.setAttr(f"{controller}.flash", keyable=True)
    cmds.setKeyframe(f"{controller}.flash", time=(1,), value=0)
    cmds.setKeyframe(f"{controller}.flash", time=(120,), value=maxValue)

    for loc, condition in stars:
        cmds.setAttr(f"{condition}.colorIfTrue", 1.0, 1.0, 1.0)
        cmds.setAttr(f"{condition}.colorIfFalse", 0.0, 0.0, 0.0)

        cmds.setAttr(f"{loc}.overrideEnabled", True)
        cmds.setAttr(f"{loc}.overrideColor", getRandomColor())

        pos = getRandomPosition(maxValue)
        cmds.move(pos[0], pos[1], pos[2], loc)
        cmds.setAttr(f"{loc}.s", *getRandomScale())
        cmds.setAttr(f"{loc}.displayHandle", lock=True)
        cmds.setAttr(f"{loc}.overrideDisplayType", lock=True)
        cmds.setAttr(f"{loc}.overrideDisplayType", lock=False)

        cmds.setAttr(f"{loc}.flashIndex", getRandomIndex(maxValue))

        cmds.connectAttr(f"{controller}.r", f"{loc}.r")
        cmds.connectAttr(f"{controller}.overrideShading", f"{loc}.overrideShading")
        cmds.disconnectAttr(f"{controller}.overrideShading", f"{loc}.overrideShading")

        cmds.connectAttr(f"{controller}.flash", f"{condition}.firstTerm")
        cmds.connectAttr(f"{loc}.flashIndex", f"{condition}.secondTerm")
        cmds.connectAttr(f"{condition}.outColorR", f"{loc}.visibility")


def renameInCmds(nodesToRename):
    for node in nodesToRename:
        cmds.rename(node, f"{node}New")
        cmds.rename(f"{node}New", node)


def queryInCmds(controller, stars):
    cmds.listConnections(f"{controller}.flash")
    for loc, _ in stars:
        cmds.objExists(loc)
        cmds.getAttr(f"{loc}.t")
        cmds.getAttr(f"{loc}.wm[0]")
        cmds.getAttr(f"{loc}.overrideDisplayType", lock=True)


def deleteInCmds(nodesToDelete):
    for toDel in nodesToDelete:
        cmds.delete(toDel)


def categorizedPerformanceTestInCmds():
    for num in NUM_NODES_LIST:
        cmds.file(new=True, force=True)
        with TotalPerfMeasurement(f"Deal with {num} nodes in maya.cmds") as measure:
            with measure.add(f"Create {num}+ nodes in maya.cmds"):
                controller, stars, nodes = createInCmds(num)

            with measure.add(f"Edit {num}+ nodes in maya.cmds"):
                editInCmds(controller, stars)

            with measure.add(f"Rename {num} nodes in maya.cmds"):
                renameInCmds(nodes)

            with measure.add(f"Query {num}+ nodes in maya.cmds"):
                queryInCmds(controller, stars)

            with measure.add(f"Remove {num} nodes in maya.cmds"):
                deleteInCmds(nodes)


def totalPerformanceTestInCmds():
    for num in REFINED_NUM_NODES_LIST:
        cmds.file(new=True, force=True)
        with PerfMeasurement(f"Deal with {num} nodes in maya.cmds"):
            controller, stars, nodes = createInCmds(num)
            editInCmds(controller, stars)
            renameInCmds(nodes)
            queryInCmds(controller, stars)
            deleteInCmds(nodes)


if __name__ == "__main__":
    categorizedPerformanceTestInCmds()
    # totalPerformanceTestInCmds()

PyMel:

import pymel.core as pmc

# execute the code from common in maya script editor first.


def createInPyMel(locCount):
    maxValue = getMaxValue(locCount)

    controller = pmc.joint()
    controller.addAttr("flash", attributeType="long", min=0, max=maxValue)
    parent = pmc.createNode("transform", n="stars")

    stars = [None] * locCount
    toDelete = [None] * locCount

    for i in range(locCount):
        condition = pmc.createNode("condition")

        loc = pmc.spaceLocator()
        pmc.parent(loc, parent)
        loc.addAttr("flashIndex", at="long", min=0, max=maxValue)

        testDel = pmc.spaceLocator()
        pmc.parent(testDel, loc)

        pmc.objExists(testDel)
        stars[i] = (loc, condition)
        toDelete[i] = testDel

    return controller, stars, toDelete


def editInPyMel(controller, stars):
    maxValue = getMaxValue(len(stars))

    controller.radius.set(10)
    controller.flash.setKeyable(True)
    pmc.setKeyframe(controller.flash, time=(1,), value=0)
    pmc.setKeyframe(controller.flash, time=(120,), value=maxValue)

    for loc, condition in stars:
        condition.colorIfTrue.set([1.0, 1.0, 1.0])
        condition.colorIfFalse.set([0.0, 0.0, 0.0])

        loc.overrideEnabled.set(True)
        loc.overrideColor.set(getRandomColor())

        loc.t.set(getRandomPosition(maxValue))
        loc.s.set(getRandomScale())
        loc.displayHandle.lock()
        loc.overrideDisplayType.lock()
        loc.overrideDisplayType.unlock()

        loc.flashIndex.set(getRandomIndex(maxValue))

        controller.r.connect(loc.r)
        controller.overrideShading.connect(loc.overrideShading)
        controller.overrideShading.disconnect(loc.overrideShading)

        controller.flash.connect(condition.firstTerm)
        loc.flashIndex.connect(condition.secondTerm)
        condition.outColorR.connect(loc.visibility)


def renameInPyMel(nodesToRename):
    for node in nodesToRename:
        node.rename(f"{node}New")
        node.rename(str(node))


def queryInPyMel(controller, stars):
    controller.flash.outputs()
    for loc, _ in stars:
        pmc.objExists(loc)
        loc.t.get()
        loc.wm[0].get()
        loc.overrideDisplayType.isLocked()


def deleteInPyMel(nodesToDelete):
    for toDel in nodesToDelete:
        pmc.delete(toDel)


def categorizedPerformanceTestInPyMel():
    for num in NUM_NODES_LIST:
        pmc.system.newFile(force=True)
        with TotalPerfMeasurement(f"Deal with {num} nodes in PyMel") as measure:
            with measure.add(f"Create {num}+ nodes in PyMel"):
                controller, stars, nodes = createInPyMel(num)

            with measure.add(f"Edit {num}+ nodes in PyMel"):
                editInPyMel(controller, stars)

            with measure.add(f"Rename {num} nodes in PyMel"):
                renameInPyMel(nodes)

            with measure.add(f"Query {num}+ nodes in PyMel"):
                queryInPyMel(controller, stars)

            with measure.add(f"Remove {num} nodes in PyMel"):
                deleteInPyMel(nodes)


def totalPerformanceTestInPyMel():
    for num in REFINED_NUM_NODES_LIST:
        pmc.system.newFile(force=True)
        with PerfMeasurement(f"Deal with {num} nodes in PyMel"):
            controller, stars, nodes = createInPyMel(num)
            editInPyMel(controller, stars)
            renameInPyMel(nodes)
            queryInPyMel(controller, stars)
            deleteInPyMel(nodes)


if __name__ == "__main__":
    categorizedPerformanceTestInPyMel()
    # totalPerformanceTestInPyMel()

AL.OMX:

from AL import omx
from maya import cmds
from maya.api import OpenMaya as om2

# execute the code from common in maya script editor first.


def createInOMX(locCount, immediate):
    maxValue = getMaxValue(locCount)
    modifier = omx.newModifier()
    if immediate:
        # usually you use omx.currentModifier() but it is not guaranteed
        # to be a immediate one, like in our case here.
        modifier._immediate = immediate

    controller = omx.createDagNode("joint", nodeName="controller")

    fnAttr = om2.MFnNumericAttribute()
    attrObj = fnAttr.create("flash", "flash", om2.MFnNumericData.kInt)
    fnAttr.setMin(0)
    fnAttr.setMax(maxValue)
    modifier.addAttribute(controller.object(), attrObj)

    parent = omx.createDagNode("transform", nodeName="stars")

    stars = [None] * locCount
    toDelete = [None] * locCount

    for i in range(locCount):
        condition = omx.createDGNode("condition")

        loc = omx.createDagNode("transform", parent=parent)
        omx.createDagNode("locator", parent=loc)
        attrObj = fnAttr.create("flashIndex", "flashIndex", om2.MFnNumericData.kInt)
        fnAttr.setMin(0)
        fnAttr.setMax(maxValue)
        modifier.addAttribute(loc.object(), attrObj)

        testDel = omx.createDagNode("transform", parent=parent)
        omx.createDagNode("locator", parent=testDel)

        testDel.isValid()
        stars[i] = (loc, condition)
        toDelete[i] = testDel

    modifier.doIt()
    return controller, stars, toDelete


def editInOMX(controller, stars):
    maxValue = getMaxValue(len(stars))
    modifier = omx.currentModifier()

    controller.radius.setInt(10)
    controller.flash.isKeyable = True
    modifier.commandToExecute(f"setKeyframe -attribute flash -t 1 -v 0 {controller}")
    modifier.commandToExecute(
        f"setKeyframe -attribute flash -t 120 -v {maxValue} {controller}"
    )

    for loc, condition in stars:
        condition.colorIfTrue.setCompoundDouble((1.0, 1.0, 1.0))
        condition.colorIfFalse.setCompoundDouble((0.0, 0.0, 0.0))

        loc.overrideEnabled.setBool(True)
        loc.overrideColor.setInt(getRandomColor())

        loc.t.setCompoundDouble(getRandomPosition(maxValue))
        loc.s.setCompoundDouble(getRandomScale())

        # here we don't care about plug state change undoability as the whole
        # node creation is done in the same XModifier.
        # otherwise we would use loc.displayHandle.setLocked(True)
        loc.displayHandle.isLocked = True
        loc.overrideDisplayType.isLocked = True
        loc.overrideDisplayType.isLocked = False

        loc.flashIndex.setInt(getRandomIndex(maxValue))

        controller.r.connectTo(loc.r)
        controller.overrideShading.connectTo(loc.overrideShading)
        loc.overrideShading.disconnectFromSource()

        controller.flash.connectTo(condition.firstTerm)
        loc.flashIndex.connectTo(condition.secondTerm)
        condition.outColorR.connectTo(loc.visibility)

    modifier.doIt()


def renameInOMX(nodesToRename):
    modifier = omx.currentModifier()
    for node in nodesToRename:
        transformName = str(node)
        modifier.renameNode(node.object(), f"{transformName}New")
        modifier.renameNode(node.object(), f"{transformName}")

    modifier.doIt()


def queryInOMX(controller, stars):
    controller.flash.destinations()
    for loc, _ in stars:
        loc.isValid()
        loc.t.get()
        loc.wm[0].get()
        loc.overrideDisplayType.isLocked


def deleteInOMX(nodesToDelete):
    modifier = omx.currentModifier()
    for toDel in nodesToDelete:
        modifier.deleteNode(toDel.object())
        modifier.doIt()


def categorizedPerformanceTestInOMX():
    for num in NUM_NODES_LIST:
        for immediate in (True, False):
            cmds.file(new=True, force=True)
            with TotalPerfMeasurement(
                f"Deal with {num} nodes in AL.omx, immediate={immediate}"
            ) as measure:
                with measure.add(
                    f"Create {num}+ nodes in AL.omx, immediate={immediate}"
                ):
                    controller, stars, nodes = createInOMX(num, immediate=immediate)

                with measure.add(f"Edit {num}+ nodes in AL.omx, immediate={immediate}"):
                    editInOMX(controller, stars)

                with measure.add(
                    f"Rename {num} nodes in AL.omx, immediate={immediate}"
                ):
                    renameInOMX(nodes)

                with measure.add(
                    f"Query {num}+ nodes in AL.omx, immediate={immediate}"
                ):
                    queryInOMX(controller, stars)

                with measure.add(
                    f"Remove {num} nodes in AL.omx, immediate={immediate}"
                ):
                    deleteInOMX(nodes)


def totalPerformanceTestInOMX():
    for immediate in (True, False):
        for num in REFINED_NUM_NODES_LIST:
            cmds.file(new=True, force=True)
            with PerfMeasurement(
                f"Deal with {num} nodes in AL.omx, immediate={immediate}"
            ):
                controller, stars, nodes = createInOMX(num, immediate=immediate)
                editInOMX(controller, stars)
                renameInOMX(nodes)
                queryInOMX(controller, stars)
                deleteInOMX(nodes)


if __name__ == "__main__":
    categorizedPerformanceTestInOMX()
    # totalPerformanceTestInOMX()

maya.api.OpenMaya (OM2):

from maya.api import OpenMaya as om2
from maya import cmds

# execute the code from common in maya script editor first.


def createInOM2(modifier, locCount):
    maxValue = getMaxValue(locCount)

    controller = modifier.createNode("joint")
    modifier.renameNode(controller, "controller")

    fnAttr = om2.MFnNumericAttribute()
    attrObj = fnAttr.create("flash", "flash", om2.MFnNumericData.kInt)
    fnAttr.setMin(0)
    fnAttr.setMax(maxValue)
    modifier.addAttribute(controller, attrObj)

    parent = modifier.createNode("transform")
    modifier.renameNode(parent, "stars")

    stars = [None] * locCount
    toDelete = [None] * locCount
    for i in range(locCount):
        condition = om2.MDGModifier.createNode(modifier, "condition")

        loc = modifier.createNode("transform", parent=parent)
        modifier.createNode("locator", parent=loc)
        attrObj = fnAttr.create("flashIndex", "flashIndex", om2.MFnNumericData.kInt)
        fnAttr.setMin(0)
        fnAttr.setMax(maxValue)
        modifier.addAttribute(loc, attrObj)

        testDel = modifier.createNode("transform", parent=parent)
        modifier.createNode("locator", parent=testDel)

        om2.MObjectHandle(testDel).isValid()
        stars[i] = (loc, condition)
        toDelete[i] = testDel

    modifier.doIt()
    return controller, stars, toDelete


def editInOM2(modifier, controller, stars):
    maxValue = getMaxValue(len(stars))
    controllerFn = om2.MFnDependencyNode(controller)
    plug = controllerFn.findPlug("radius", True)
    modifier.newPlugValueInt(plug, 10)

    plug = controllerFn.findPlug("flash", True)
    plug.isKeyable = True
    modifier.commandToExecute(
        f"setKeyframe -attribute flash -t 1 -v 0 {controllerFn.name()}"
    )
    modifier.commandToExecute(
        f"setKeyframe -attribute flash -t 120 -v {maxValue} {controllerFn.name()}"
    )

    colorIfTrueNames = ("colorIfTrueR", "colorIfTrueG", "colorIfTrueB")
    colorIfFalseNames = ("colorIfFalseR", "colorIfFalseG", "colorIfFalseB")

    translation = ("tx", "ty", "tz")
    scale = ("sx", "sy", "sz")
    for loc, condition in stars:
        locFn = om2.MFnDependencyNode(loc)
        conditionFn = om2.MFnDependencyNode(condition)
        for trueAttr in colorIfTrueNames:
            plug = conditionFn.findPlug(trueAttr, True)
            modifier.newPlugValueDouble(plug, 1.0)

        for falseAttr in colorIfFalseNames:
            plug = conditionFn.findPlug(falseAttr, True)
            modifier.newPlugValueDouble(plug, 0.0)

        plug = locFn.findPlug("overrideEnabled", True)
        modifier.newPlugValueBool(plug, True)

        plug = locFn.findPlug("overrideColor", True)
        modifier.newPlugValueInt(plug, getRandomColor())

        for name, value in zip(translation, getRandomPosition(maxValue)):
            plug = locFn.findPlug(name, True)
            modifier.newPlugValueDouble(plug, value)

        for name, value in zip(scale, getRandomScale()):
            plug = locFn.findPlug(name, True)
            modifier.newPlugValueDouble(plug, value)

        plug = locFn.findPlug("displayHandle", True)
        plug.isLocked = True

        plug = locFn.findPlug("overrideDisplayType", True)
        plug.isLocked = True
        plug.isLocked = False

        plug = locFn.findPlug("flashIndex", True)
        modifier.newPlugValueInt(plug, getRandomIndex(maxValue))

        src = controllerFn.findPlug("r", True)
        dst = locFn.findPlug("r", True)
        modifier.connect(src, dst)

        src = controllerFn.findPlug("overrideShading", True)
        dst = locFn.findPlug("overrideShading", True)
        modifier.connect(src, dst)
        modifier.disconnect(src, dst)

        src = controllerFn.findPlug("flash", True)
        dst = conditionFn.findPlug("firstTerm", True)
        modifier.connect(src, dst)

        src = locFn.findPlug("flashIndex", True)
        dst = conditionFn.findPlug("secondTerm", True)
        modifier.connect(src, dst)

        src = conditionFn.findPlug("outColorR", True)
        dst = locFn.findPlug("visibility", True)
        modifier.connect(src, dst)

    modifier.doIt()


def renameInOM2(modifier, nodesToRename):
    for node in nodesToRename:
        transformName = str(node)
        modifier.renameNode(node, f"{transformName}New")
        modifier.renameNode(node, f"{transformName}")

    modifier.doIt()


def queryInOM2(controller, stars):
    translation = ("tx", "ty", "tz")
    controllerFn = om2.MFnDependencyNode(controller)
    controllerFn.findPlug("flash", True).destinations()
    for loc, _ in stars:
        om2.MObjectHandle(loc).isValid()
        locFn = om2.MFnDependencyNode(loc)
        [locFn.findPlug(n, True).asDouble() for n in translation]
        wm0Plug = locFn.findPlug("wm", True).elementByLogicalIndex(0)
        attrData = om2.MFnMatrixData(wm0Plug.asMObject()).matrix()
        [attrData[i] for i in range(len(attrData))]
        locFn.findPlug("overrideDisplayType", True).isLocked


def deleteInOM2(modifier, nodesToDelete):
    for toDel in nodesToDelete:
        modifier.deleteNode(toDel)
        modifier.doIt()


def categorizedPerformanceTestInOM2():
    for num in NUM_NODES_LIST:
        cmds.file(new=True, force=True)
        with TotalPerfMeasurement(f"Deal with {num} nodes in om2") as measure:
            modifier = om2.MDagModifier()
            with measure.add(f"Create {num}+ nodes in om2"):
                controller, stars, nodes = createInOM2(modifier, num)

            with measure.add(f"Edit {num}+ nodes in om2"):
                editInOM2(modifier, controller, stars)

            with measure.add(f"Rename {num} nodes in om2"):
                renameInOM2(modifier, nodes)

            with measure.add(f"Query {num}+ nodes in om2"):
                queryInOM2(controller, stars)

            with measure.add(f"Remove {num} nodes in om2"):
                deleteInOM2(modifier, nodes)


def totalPerformanceTestInOM2():
    for num in REFINED_NUM_NODES_LIST:
        cmds.file(new=True, force=True)
        with PerfMeasurement(f"Deal with {num} nodes in om2"):
            modifier = om2.MDagModifier()
            controller, stars, nodes = createInOM2(modifier, num)
            editInOM2(modifier, controller, stars)
            renameInOM2(modifier, nodes)
            queryInOM2(controller, stars)
            deleteInOM2(modifier, nodes)


if __name__ == "__main__":
    categorizedPerformanceTestInOM2()
    # totalPerformanceTestInOM2()