SOLVED Option to wait for c4d to catch up with script ? Memory wise and EventAdd?

Dear Developers,

I have a strange problem. My Polygon merging script with mcomand join runs smoothly, finishing in 14 seconds (500Mb File) and Cinema 4D needs 1Hour on one thread to display me the result !

Is there a way to wait inside the script till c4d alocated memory, placed the new object until the loop continouse?

This idea could be wrong, but hence the script runs till the end I do not know how to handle this behavior.

Any ideas ?
kind regards
mogh

Hi @mogh,

thanks, but I will probably only have a chance to look at it tomorrow. Happy coding and rendering in the mean time 😉

Cheers,
Ferdinand

Hello @mogh,

thank you for reaching out to us. First of all I am sorry for the long response time, but the weekend and other topics were "in the way" 😉

Regarding your question: This all sounds a bit mysterious. One hour seems pretty long for just processing a scene. Could you provide a full example file which takes this long (you can send your data to be treated confidently to sdk_support(at)maxon.net) ? At least the fragment of your scene you already sent us, is being processed without problems on my machine.

In principle you have might have to decouple your processing (the whole "join"-thingy) from the main thread and then check if the Cinema is still chewing on your data. A Script-Manger script alone will probably not be sufficient for that, you might need a plugin or at least a dialog. But it is really hard to tell from the outside without seeing what is going on.

Cheers,
Ferdinand

hi zipit, no worries,
thank you for your time!

Yes it is misterious, thats why I asked so vague. You can reproduce a heavy data import by putting my object sent to you into a cloner (5x1x5) and convert it (CSO) -
Running the below script takes about 4 seconds to run and about 1Minute plus to display with the same weird finished but not fnished behavior.

Use below script, set your C4D to only have one window otherwise I have status bar issues!
The status bar displays when its fnished with main() the console lags behind but displays 4 secons !

Latest Version, difference to the slim version is mainly with some quality of live code.

by the way, scripts running for 1hour + are no strangers here - we often start them over night ... thats kinda normal the behavior is not
kind regards mogh

#!py3
import c4d, sys, os, math
from c4d import gui
from c4d.documents import GetActiveDocument
#Version 1.5

import cProfile, pstats 
#import time
def profile(func):
    # A simple profiling decorator.
    profiler = cProfile.Profile()

    def wrapper(*args, **kwargs):
        result = None
        try:
            result = profiler.runcall(func, *args, **kwargs)
        except Exception as error:
            raise error
        finally:
            stats = pstats.Stats(profiler)
            # To get the full profile do this.
            stats.strip_dirs().sort_stats("cumtime").print_stats()
            t = round(stats.total_tt, 3)
            #print (f"{func.__name__} took {t} sec.\n")
        return result
    return wrapper

#@profile
def gime_time (milliseconds):
    hours,milliseconds = divmod(milliseconds, 3600000)
    minutes, milliseconds = divmod(milliseconds, 60000)
    seconds = float(milliseconds) / 1000
    s = "%i:%02i:%06.3f" % (hours, minutes, seconds)
    return s

#@profile
def GetNextObject(op):
    if not op: return
    if op.GetDown(): return op.GetDown()
    while op.GetUp() and not op.GetNext():
        op = op.GetUp()
    return op.GetNext()

#@profile
def get_all_objects (op):
    allachsen_list = list()
    all_objects_list = list()
    while op:
        if op.GetName() == 'Achsen-Objekt' or op.GetName() == 'Axis' :
            allachsen_list.append(op)
        all_objects_list.append(op)
        op = GetNextObject(op)
    #return {'all_objects_list' : all_objects}, allachsen
    return all_objects_list, allachsen_list

#@profile
def statusbar (counter, secondcounter):
    #print (secondcounter, int((100*secondcounter)/counter) )
    if int((100*secondcounter)/counter) in [10,20,30,40,50,60,70,80,90,100]:
        #print("Textupdate at: ", int((100*secondcounter)/counter))
        c4d.StatusSetText ('%s - %s Objects are processed.' %(secondcounter, counter))
    c4d.StatusSetBar(int(100*secondcounter/counter)) #statusbar
    #c4d.gui.GeUpdateUI()

def savetorun(checkSelection = False):
    try:
        doc = GetActiveDocument()
    except Exception as e:
        print (e)
        raise TypeError("No Active Document!")
        exit()
        return False

    try:
        op = doc.GetFirstObject()
    except Exception as e:
        print (e)
        c4d.StatusSetText ('No Objects, nothing to do here.')
        raise TypeError("No Objects, nothing to do here.")
        exit()
        return False

    selected = None
    if checkSelection == True:
        selected = doc.GetActiveObject()
        if selected == None:
            c4d.StatusSetText ('No Object/s selected, nothing to do here.')
            print ("No Object/s selected, nothing to do here.")
            exit()
            return False

    return doc, op, selected

def savedoc(doc):
    path = doc.GetDocumentPath()
    name = doc.GetDocumentName()
    name = name[ :-4 ] + "_conectedaxis.c4d"
    filename = os.path.join(path, name)
    saveflags = (c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST | c4d.SAVEDOCUMENTFLAGS_DIALOGSALLOWED)
    c4d.documents.SaveDocument(doc, filename, saveflags, format=c4d.FORMAT_C4DEXPORT)

#@profile
def JoinCommand(doc, op):
    """ Apply a join command to the object passe as argument
    :param      op: the object to apply current state to object to.
    :type: BaseObject
    :return: the result of the command or raise an error if command failed
    :rtype: BaseObject
    """
    # null = c4d.BaseObject(c4d.Onull)
    # #for o in op.GetChildren:
    # op.InsertUnder(null)
    #settings = c4d.BaseContainer()
    #settings[c4d.MDATA_JOIN_MERGE_SELTAGS] = True
    # bc = settings,
    
    res = c4d.utils.SendModelingCommand(command = c4d.MCOMMAND_JOIN,
                                list = [op],
                                mode = c4d.MODELINGCOMMANDMODE_ALL,                                
                                doc = doc)

    # Cheks if the command didn't failed
    if res is False:
        raise TypeError("return value of Join command is not valid")
    elif res is True:
        print ("Command successful. But no object.")
    elif isinstance(res, list):
        #if c4d.GetC4DVersion() < 21000: res[0].SetAbsPos(c4d.Vector())
        op.Remove()
        return res[0] # Returns the first item containing the object of the list.  ??? GetClone() ???
    
#@profile
def set_point_object_transform(node, transform):
    """Sets the global transform of a point object while keeping its points in place.

    Args:
        node (c4d.PointObject): The point object to move the axis for.
        transform (c4d.Matrix): The new global transform for the object.

    Raises:
        TypeError: When node or transform are not of specified type.
    """
    if (not isinstance(node, c4d.PointObject) or
        not isinstance(transform, c4d.Matrix)):
        msg = f"Illegal argument types: {type(node)}{type(transform)}"
        raise TypeError(msg)

    #mg = node.GetMg()
    # Move the points in the global frame and then into the new frame.
    points = [p * ~transform for p in node.GetAllPoints()]
    # Set the points and stuff ;)
    node.SetAllPoints(points)
    node.Message(c4d.MSG_UPDATE)
    node.SetMg(transform)
    
#@profile
def joinmanagment(n):
    # n "Axis" null will be not alive in a few steps get everything we need from it
    if n.GetUp() :
        parent = n.GetUp()
    else:
        print ("No Parent To Axis Null. Probably not save to run this sript anyway.")
        c4d.StatusClear()
        c4d.StatusSetText ('No Parent found! - Probalby mo CAD import Doc. Script Stopped.')
        exit()
        return False

    newobject = JoinCommand(doc, n) # combine the poly objects

    if not newobject.IsAlive():
        raise TypeError("Object is not alive.")
        return False

    newobject.SetName(str(parent.GetName()))
    newobject.InsertUnder(parent)
    set_point_object_transform(newobject, n.GetMg()) # set points with global matrix from parent n "Axis"

#@profile
def main():
    c4d.CallCommand(13957) # Konsole löschen
    now = c4d.GeGetTimer() # start stopwatch
    # Savetorun: Set True to check for slected objects and get them returned or throw an error, otherwise slected is None.
    doc, op , selected = savetorun()

    c4d.StatusSetSpin()
    all_objects, allachsen = get_all_objects(op) # get two lists

    null_counter = len(allachsen)

    if null_counter == 0: # check if found something to do.
        c4d.StatusClear()
        c4d.StatusSetText ('No Axis Objects found, nothing to do here.')
        print ("No Axis Objects found, nothing to do here.")
        exit()

    counter = len(all_objects)
    secondcounter = 0

    print (counter,' Objects' ,'\n' ,null_counter,' Nulls and their children to connect' ,'\n' ,'----------------------------------------------------' )
    c4d.StatusSetText ('%s Objects are processed.' %(null_counter))

    for n in allachsen:
        secondcounter += 1
        statusbar(null_counter, secondcounter)
        joinmanagment(n)
        #if joinmanagment(n) == False:            break

    print ('----------------------------------------------------')
    print ('END OF SCRIPT %s ms ' %(gime_time(c4d.GeGetTimer() - now))   )
    #savedoc(doc)
    c4d.StatusClear()
    c4d.StatusSetText ('Script finished - C4D needs a while to diplay the new viewport?!')
    c4d.EventAdd()        # update cinema 4d
    return True
    
if __name__=='__main__':
    main()

Hi @mogh,

I have posted here a variant of your script. To simplify things, I would ask you to first run this script and see if it does change anything about your problem. Please also try to run the script with #c4d.EventAdd() uncommented in line 155.

Cheers,
Ferdinand

slightly,

the waiting is now in the beginning, with no or sporadic statusbar updates in between and some waiting at the end.

While this seems fine in a generall sense hence the script now takes its time more reasonably, its a bummer that I cannot inform the user about the status of the script in a meaningfull manner.

C4D "hangs and calculates" and is fnished at some point. If I did not know better I would suspect C4D to be crashed and kill it as a user.

I suspect that the c4d.utils.SendModelingCommand does not allow a better solution at this point and only a selfmade C++ version would.


Also my hopes for a faster script are shattered, hence the simplified script you posted takes more or less the same time (console reports 10sec but its actually 20seconds) as my 5 year old callcomand script (also 20 seconds) which is a bummer.

I am still a bit lost on how to pursue my "optimzing scripts" endeavor at this point. (which is off topic here).

nevertheless, thank you for your time

kind regards
mogh

Hi,

this was just a question, to sort out that my does not script accidentally fix your problem 😉 As said in the beginning, if you want more insight into this, I would invite you to provide some test data and the script you are using where you would consider the processing time unreasonable. As already said, you can use the sdk support mail address to provide such data.

Cheers,
Ferdinand

@zipit said in Option to wait for c4d to catch up with script ? Memory wise and EventAdd?:

I would invite you to provide some test data

You can reproduce a heavy data import by putting my object sent to you into a cloner (5x1x5 ... or even more) and convert it (C). No need to send hundreds of megabites through email.

Use your script not my mess if you like.

kind regards
mogh

Hi,

so I did that. I took the wheel thingy you did send us, put it in a 5³ cloner, collapsed the cloner and ran the script. It had about 50k objects in the scene and it took me about 65 seconds on my machine (something i7 and 32GB Ram) to chew them down to 151 objects. This is probably not as fast as one would like it to be, but you have to realize that these are a lot of join commands and in this case about 4 million vertices and an equal amount of normals to chew through and reorganize.

I would not consider this to be extremely slow. If you want to to make this more responsive, you will simply have to convert your data in batches. This also could be simply a RAM issue, because you will need a multiples of what the scene geometry takes in RAM. Doing it in batches will also address this, because it will give Cinema and Python's GC time to make room by deallocating unused stuff. But again, one hour seems very long, but without actual test data, this is all just a guessing game.

Cheers
Ferdinand

did sent email with raw data

kind regards
mogh

Hi @mogh,

thanks, but I will probably only have a chance to look at it tomorrow. Happy coding and rendering in the mean time 😉

Cheers,
Ferdinand