pjkundert / cpppo

Communications Protocol Python Parser and Originator -- EtherNet/IP CIP
https://hardconsulting.com/products/6-cpppo-enip-api
Other
332 stars 109 forks source link

CPU usage #50

Open tdesroches opened 6 years ago

tdesroches commented 6 years ago

I've been playing around with this library and trying to collect tags from a controllogix at fairly high speed, 100ms or less. It seems to require quite a bit more CPU load than I expected. For 6 REAL variable polled every 100ms I sit around 30% of a i7-4710MQ CPU @ 2.50GHz × 8 CPU. The 30% is the process on a single thread not the entire processor. When I read 3 of those tags its at about 20%. A 10% jump for 3 additional tags. I plan on probably around 100 tags in total.

I'm using the Polling api. Is this library not really capable of reading many tags rapidly? Is there perhaps a better api I could use? Or is this a limitation of python and I should be looking at using something like C/C++?

tdesroches commented 6 years ago

I've also posted my code, maybe I'm just not programming effectively. It's pretty much the example slightly modified

import cpppo
import threading
from cpppo.server.enip import poll
from cpppo.server.enip.get_attribute import proxy as device # ControlLogix
import time
params = ["AI_XAxis[0].Status.rActualPosition","AI_YAxis[0].Status.rActualPosition","AI_ZAxis[0].Status.rActualPosition",
                    "AI_XAxis[1].Status.rActualPosition","AI_YAxis[1].Status.rActualPosition","AI_ZAxis[1].Status.rActualPosition"]

def failure( exc ):
    failure.string.append( str(exc) )
failure.string      = [] # [ <exc>, ... ]

def process( par, val ):
    process.values[par] = val
process.done        = False
process.values      = {} # { <parameter>: <value>, ... }

poller          = threading.Thread(
    target=poll.poll, kwargs={
        'proxy_class':  device,
        'address':      ("192.168.60.30", 44818),
        'cycle':        0.1,
        'timeout':      0.5,
        'process':      process,
        'failure':      failure,
        'params':       params,
        'pass_thru':    True
    })
poller.start()
try:
    while True:
        while process.values:
            print("X: ", process.values.pop("AI_XAxis[0].Status.rActualPosition", "???"))
            print("Y: ", process.values.pop("AI_YAxis[0].Status.rActualPosition", "???"))
            print("Z: ", process.values.pop("AI_ZAxis[0].Status.rActualPosition", "???"))
            print("X2: ", process.values.pop("AI_XAxis[1].Status.rActualPosition", "???"))
            print("Y2: ", process.values.pop("AI_YAxis[1].Status.rActualPosition", "???"))
            print("Z2: ", process.values.pop("AI_ZAxis[1].Status.rActualPosition", "???"))
            #par,val        = process.values.popitem()
            #print( "%s: %16s == %r" % ( time.ctime(), par, val ))
        while failure.string:
            exc     = failure.string.pop( 0 )
            print( "%s: %s" %( time.ctime(), exc ))
        time.sleep( 0.1 )
finally:
    process.done        = True
    poller.join()
pjkundert commented 6 years ago

That’s about the CPU usage you can expect for that poll rate. You can perhaps double that using pypy, but the parser is running in pure python, so it’s peaking out at about 20-30 polls/sec per i7 core...

I’m working on a Connected session support thread that will allow us to establish a spontaneous update stream, so we’ll only need to parse incoming data. That should help a lot, I think.

However, you are right - you’ll need to go to a more efficient parser for higher poll rates. The right solution is always spontaneous report of changes though.

tdesroches commented 6 years ago

Ok thanks, I'll try pypy and see what I can squeeze out of it.

What is the timeline looking like for the connected session?