Author Topic: Python TCP server - CPU usage continually increasing over time  (Read 7642 times)

0 Members and 1 Guest are viewing this topic.

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Python TCP server - CPU usage continually increasing over time
« on: January 26, 2018, 08:33:07 pm »
Please take it easy on me, I only started learning Python last week, and have cobbled this together, so I am fully aware that it will look a pile of crap to the proper programmers on here!

This is a Python3 script running on Ubuntu Server on a Raspberry Pi 3.  It is emulates a Modbus TCP slave (server).  This code runs in it's own thread, the main thread pulls data from an ASCII TCP stream and puts the required values into variable, which the Modbus thread then maps into a virtual register space, and responds to Modbus requests from the Master (client).  The requests come in about 6 times a second, there is only one client, and every request is the same.  (This whole bodge job is to get live data into a a proprietary control system that only talks Modbus TCP)

It does work, and seems to work well initially.  (I know I am playing with fire by assuming that the complete Modbus request is received on a single recv() call)

When it starts, Python consumes around 3% of one CPU core, but this creeps up and up and up.  (I am seeing this via htop, with tree view enabled.  This child thread is using the CPU - if the parent shows 10% this thread show 9% etc)
  After about 10 hours it's at 90% and things fall apart.
Whilst a total beginner with Python and socket programming, I cannot understand why this happens over time.  The loop is doing exactly the same thing after the first minute than it is after 10 hours, so why the increase in CPU usage?  Memory usage does not increase.

Anyway, here is the monstrosity.  At this point, please don't comment on the rubbish way I'm doing things, just on why the CPU usage is increasing!  I'm sure it's a schoolboy error with my handling of the sockets.  (As an aside, I am extremely impressed with Python, in that with nothing more than a PDF tutorial downloaded for free, Mr Google and a couple of days, I had something running that does the job, if only for a few hours!

Code: [Select]
class modbusThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
### Now we define what will actually run as that thread - the Modbus server itself
def run(self):
global modbus_outer_loops
global modbus_inner_loops
global modbus_requests_served 
# create 64 kbyte address space for Modbus registers
# each register is 16 bits wide, we are using 32 bit Floats, so each value takes two registers
# registers thus must be addressed 0, 2, 4 etc...
# any registers read that have not had WITS data placed in them will read -777.1
register_address_space = bytearray(struct.pack(">f", -777.1)) * 16384
wanted_record_items = [ [] ] * 100 # create empty list of lists
############################## WHAT DATA ITEMS DO WE WANT? ###############################
wanted_record_items[1] = [40,41]
wanted_record_items[66] = [8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]


host = socket.gethostname() #MODBUS_IP_ADDRESS
print ("MODBUS- Hostname:", host)
port = 502
sinewave_degrees = 0.0

while True: ### main outer loop.  Should only run to initially bind to the TCP port.
modbus_outer_loops += 1
hard_restart_required = False
print ("MODBUS- Creating modbus_socket")
modbus_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print ("MODBUS- Binding to",host,port)
try: # try to bind to the TCP port, will fail if network not up...
modbus_socket.bind((host,port))
except:
print ("MODBUS- Cannot bind to the Modbus TCP port! Is the network up?")
time.sleep(RESTART_WAIT)
continue

modbus_socket.listen(0)

while True: #############       Inner loop, deals with connections from client
modbus_inner_loops += 1
print ("MODBUS- Waiting for incoming connection on",host,port,"...")
client_socket,addr = modbus_socket.accept()
client_socket.settimeout(TCP_TIMEOUT)
print ("MODBUS- ",modbus_outer_loops,", ",modbus_inner_loops,", Incoming connection from %s" % str(addr))

while True:
sinewave_degrees += 0.25
if sinewave_degrees >= 360:
sinewave_degrees = 0
sinewave_float = math.sin(math.radians(sinewave_degrees)) * 50
sinewave_float += 50
sinewave_int = int(sinewave_float)

# Read the wanted data into the register space
register = 0 # Register 0 gets the test signal (sine wave)
register_address_space[0:3] = bytearray(struct.pack(">f", sinewave_float))
register += 2 # We use TWO 16 bit registers per float!
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
register_address_space[register * 2:(register * 2)+3] = bytearray(struct.pack(">f", record_item[record][item]))
register += 2

try:
request = client_socket.recv(SOCKET_BUFFER)
except:
print ("MODBUS- Connection failed.")
print ("MODBUS- Closing socket and restarting listener.")
break
if not request:
print ("MODBUS- Connection established but no data received. (Connection closed by peer?)")
print ("MODBUS- Hard restarting. Killing server socket and restarting listener from scratch")
hard_restart_required = True
break

start_time = timer()



# Build the Modbus response
# print ("MODBUS- -Request:"," ".join("%02x" % b for b in request))
transactionID0 = request[0]
transactionID1 = request[1]
protocol0 = request[2]
protocol1 = request[3]
unitID = request[6]
function_code = request[7]
start_register_hi_byte = request [8]
start_register_lo_byte = request[9]
number_of_registers_hi_byte = request[10]
number_of_registers_lo_byte = request[11]

function_code = 3 # function code 3 is all you get! take it or leave it!

start_register = start_register_lo_byte + (start_register_hi_byte * 256)
number_of_registers = number_of_registers_lo_byte + (start_register_hi_byte * 256)

byte_count = number_of_registers * 2 # registers are 16 bit
length = byte_count + 3 # data length plus, byte count, func code and unit ID
length_hi_byte = length >> 8
length_lo_byte = length & 255

response_list = [transactionID0, transactionID1, protocol0, protocol1, length_hi_byte, length_lo_byte, unitID, function_code, byte_count] # add all that shit together as a list

response_list += register_address_space[start_register * 2 : (start_register * 2) + (number_of_registers * 2)] # add the data onto the end of the response

response_bytes = bytes(response_list) # turn it into a byte string



# print ("MODBUS- Response:"," ".join("%02x" % b for b in response_bytes))
end_time = timer()
client_socket.send(response_bytes)
modbus_requests_served += 1
print ("MODBUS- ",modbus_outer_loops,", ",modbus_inner_loops," - ",modbus_requests_served," requests served. Response time = ", end_time - start_time, sep="")
time.sleep(0.025)
print ("MODBUS- EXITING INNER LOOP-------------------------------")
client_socket.close()
if hard_restart_required:
hard_restart_required = False
break
time.sleep(MODBUS_RESTART_WAIT)
print ("MODBUS- EXITING OUTER LOOP---------------------")
modbus_socket.close()



The sinewave stuff is just as a test signal.
I have moved the block that moves the data into the register space to before the recv() call to see if that helps.  It was done after the recv(), and takes no more than 0.01 seconds to execute.

Many thanks, and I promise I will do a much better job with v2, but why oh why the steady increase in CPU usage?
« Last Edit: January 26, 2018, 08:40:50 pm by Delta »
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #1 on: January 27, 2018, 07:29:49 am »
I have done some more research, and I now think that the problem lies not with the sockets, but with sharing a global variable between the parent and child threads.

Using strace -c -fp PID, I see that the calls to futex (and the errors) increase as time goes on.  I understand this is due to spinlocks as each thread waits to access the global variable (or list to be precise).  I understand that it is bad practice to use global variables in this way, but my question remains: why does this get worse over time?
 

Offline mrflibble

  • Super Contributor
  • ***
  • Posts: 2051
  • Country: nl
Re: Python TCP server - CPU usage continually increasing over time
« Reply #2 on: January 31, 2018, 03:02:57 pm »

Okay, 215 views and no replies. I read it a couple of days ago and didn't really notice one single item that would be the cause. So I thought I'd leave it to other people who maybe noticed something. Well, so much for that plan...

When I said I didn't really notice one single thing that would be the cause I lied. I did see one single item that's a bit of a red flag (for me) in python:

class modbusThread(threading.Thread):

Python threading combined with the behaviour you describe makes me think of spinlocks slowly accumulating somewhere. Main advice based on past experience is that, if at all possible, get rid of threads in python. If it's not at all possible, make it possible, and then get rid of those threads anyways. No doubt it can be done, but more trouble than it's worth IMO.

For things like firing up a daemon in the background I tend to use a simple shellscript that takes care of that. So you'd have two separate processes, two separate python interpreters running. Then you have your two processes communicate through a tcp socket or fifo or whatever you deem fit. IMO that reduces the chances of having to debug "interesting things".

Random tips: Use a decent IDE, that can help you get up to speed faster. I've been using PyCharm for a while now and I really like it. Free version available here:

https://www.jetbrains.com/pycharm/download/

When getting weird cpu usage issues, a profiler can definitely help:

https://docs.python.org/3/library/profile.html

When you never modify a list, consider using a tuple. Cheaperbetterfaster, and it reduces the chance of an unintended oh-woops-modification.

When only ever using a list in a for loop, consider using a generator.

Ditto for range(). Quite often using xrange() instead of range() will do the trick.

When creating a huge empty list of lists, and then only populating a few entries, consider using dictionaries {} as alternative. Choice would depend on your access pattern.

Oh, and get a decent IDE. Did I mention that yet? It will give hints a la "use style such and such here". Or in other words, free help with coding style, which in turn may increase likelihood of code reviews. ;) What I like about pycharm is, it does all that .... and best of all, this behaviour can be turned off. Sensible default settings, but if you want popup such-and-such to go away, you can actually find the configuration option, and then turn it off.

Anyways, good luck with your modbus script. :)
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #3 on: February 01, 2018, 11:26:08 am »
Thank you for your detailed reply Mr F!

I have since split the script into an ASCII thread and Modbus thread, all the main script does is start the two worker threads.  I have also added a lock for the big global list, and an event so ASCII thread to signal to the Modbus thread when new data is available.  This keeps CPU usage below 1%, but has dramatically reduced the rate at which Modbus responses are served.

Thanks for the advice of avoiding threading in Python if possible.  I will look into rewriting it all and using non-blocking sockets so the ASCII receiving and Modbus serving can be done within the same loop.
 

Offline imidis

  • Frequent Contributor
  • **
  • Posts: 426
  • Country: ca
Re: Python TCP server - CPU usage continually increasing over time
« Reply #4 on: February 01, 2018, 12:33:10 pm »
I'm not familiar enough with python. But it reminds me of a couple decades ago running a linux email server at home. It would be fine for a while then would get to a point it would be completely unresponsive. One configuration flag in sendmail was not set and causing an endless loop with itself up to the point it would take down the server.  :palm:
Gone for good
 

Offline paulca

  • Super Contributor
  • ***
  • Posts: 4031
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #5 on: February 01, 2018, 03:17:19 pm »
You are creating listening sockets in a loop and then inside a nested loop accepting connections?

That isn't how to handle client connections.  You will only be able to handle one connection at a time. 

You have so many nested loops allocating memory and looping over arrays that it's possible you are appending to one of them accidentally.  Does memory foot print also increase?

I would divide the code up into smaller parts. 

Try this, it will make things a LOT easier for the sockets.
https://docs.python.org/2/library/socketserver.html

I'm in work, I can post my code for handling TCP clients and UDP updates later.  It also uses a global variable or two.

As a side question, if you are just reading ModBus and sending it to a single end point, why not just send a UDP packet?
"What could possibly go wrong?"
Current Open Projects:  STM32F411RE+ESP32+TFT for home IoT (NoT) projects.  Child's advent xmas countdown toy.  Digital audio routing board.
 

Online djacobow

  • Super Contributor
  • ***
  • Posts: 1151
  • Country: us
  • takin' it apart since the 70's
Re: Python TCP server - CPU usage continually increasing over time
« Reply #6 on: February 01, 2018, 04:36:20 pm »

Multi-threaded programming has never really been Python's strongest suit. Most Python programmers who need concurrency choose twisted, gevent, or now, the one with the most support and built into Python, asyncio. Under asyncio, you only have one thread, but essentially all your code becomes nonblocking and there is an event loop that calls all the "threads". You should look at it. A drawback for me for asyncio is that it is kind of an all-or-nothing choice. You can start a program and then add some async behavior with asyncio. You sort of have to dive into the deep end.

If you are going to stick with threads, then let me suggest a very simple model where the threads communicate through queues and ONLY through queues. Python even has the Queue module for this. Particular for a master / worker(s) arrangement, this works nicely with little confusion. The master can create some work, and push it into a queue and move on and do whatever (answer the next tcp request, for example) and the workers just pop from the Queue and work on it and when they are done they can push the answer into a results queue, which the master thread can check whenever.

You got some advice to use an IDE. Most people prefer them, but I do not. I use a standalone text editor with syntax highlighting. It also had features for syntax checking which I leave on, for PEP-8 compliance (which I leave OFF), and even code completion (which I generally find not that useful, but others swear by it).

Since you're using python3, you are already using "xrange" (python2's "xrange" is python3's "range", and the old range is gone)

Best,
Dave J
 

Offline paulca

  • Super Contributor
  • ***
  • Posts: 4031
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #7 on: February 01, 2018, 04:52:29 pm »
Python threads are apparently serialized anyway.  There is comment in the documentation stating that due to the way they do threading only one thread will be executing at a time. 

As to whether a thread can be interrupted while carrying out a non-atomic operation is another question entirely.

That is not to say you can't do proper multi-processor parallel execution of threads in Python, I'm sure you can, but I don't think the default out of the box threading lib supports it.

The OPs code, as far as I can determine could just delete the threading and put the run(self) into a main method.  It would run the same.  It only supports one client at a time and thus does not require threads, except to free the main method for other executions while waiting on socket.accept()

On NBIO.  It's really the empires new clothes.  Behind the curtain of blocking IO the OS is using non-blocking "select" polling and it's own events model, then sending signals to release your code from it wait state.  Below that layer the hardware is using signals and wait states.  Sleep, wake on interrupt.

It does give you control and avoids using threading libraries, but the under lying mechanisms are much the same. 

The other very obvious downside of NBIO is it usually requires polling.  Polling is much more expensive, especially on a concurrent system with competing threads.  you either poll slowly which gives your process poor performance or you poll fast (spin lock style) which gives other processes a hard time.

You don't get anything for free.

That said there is a tendency for people to use too many threads badly and end up in a complicated mess.  A "Threading model" is a very important design decision to make early in a project.

For the OPs purposes, the basic Python SocketServer lib is all he needs and multi-threading is absolutely fine.  I don't think there will be much if any conflict with the shared variables, even if he supports multiple clients.  At the very worse case he will get a dirty read on a multi-byte write from the ModBus thread.
« Last Edit: February 01, 2018, 05:00:56 pm by paulca »
"What could possibly go wrong?"
Current Open Projects:  STM32F411RE+ESP32+TFT for home IoT (NoT) projects.  Child's advent xmas countdown toy.  Digital audio routing board.
 

Offline andersm

  • Super Contributor
  • ***
  • Posts: 1198
  • Country: fi
Re: Python TCP server - CPU usage continually increasing over time
« Reply #8 on: February 01, 2018, 05:39:24 pm »
Python threads are apparently serialized anyway.  There is comment in the documentation stating that due to the way they do threading only one thread will be executing at a time.
That depends on the implementation. In CPython ("normal" Python) only one thread can be executing in the interpreter, but if it blocks eg. on a system call then another thread can be picked for execution.

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #9 on: February 15, 2018, 10:52:28 pm »
You are creating listening sockets in a loop and then inside a nested loop accepting connections?

That isn't how to handle client connections.  You will only be able to handle one connection at a time. 

You have so many nested loops allocating memory and looping over arrays that it's possible you are appending to one of them accidentally.  Does memory foot print also increase?

I would divide the code up into smaller parts. 

Try this, it will make things a LOT easier for the sockets.
https://docs.python.org/2/library/socketserver.html

I'm in work, I can post my code for handling TCP clients and UDP updates later.  It also uses a global variable or two.

As a side question, if you are just reading ModBus and sending it to a single end point, why not just send a UDP packet?

This is a very specific application, it is not a Server in the conventional sense.  There will only ever be one connection, and always from the same client.  The loops are just to ensure that if the connection is closed or dropped, my script is ready to accept a new connection ASAP.

I am not reading Modbus, I am reading an ASCII stream.  The problematic portion of the script is the part that Serves to Modbus responses as the requests come in from the one and only client.  (Which will not talk UDP)

(I will look into that library though, thanks for the link)
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #10 on: February 15, 2018, 11:01:14 pm »
I have made a couple of improvements, such as using a queue.Queue to pass data from one thread to the other, so there are now no global variables or locks at all.

I have also found the exact portion of the code that is taking longer and longer to execute as time goes on:
Code: [Select]

start_time = timer()
register = 2
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
register_address_space[register * 2:(register * 2)+3] = bytearray(struct.pack(">f", record_item[record][item] ))
register += 2

pass # end of loop what reads from WITS list
modbus_wits_reads += 1
read_time = ( timer() - start_time ) * 1000


When the script has been running for only a few minutes, this block takes around 1ms to execute.
It has now been running for 3 hours and 24 minutes, this block has been executed 9948 times, and is now taking over 8ms to execute!  :scared:

Please please PLEASE can someone point out the stupid error I have made here?

EDIT-------
Here is the complete code for the thread, if that makes it any clearer (I know people not posting full context is an annoyance)
Code: [Select]
### Now we define what will actually run as that thread - the Modbus server itself
def run(self):
modbus_outer_loops = 0
modbus_inner_loops = 0
modbus_requests_served = 0
modbus_wits_reads = 0
empty_queues = 0
QUEUE_GET_TIMEOUT = 0.1
libc = 'libc.so.6'
for cmd in (186, 224, 178):
TID = ctypes.CDLL(libc).syscall(cmd)
if TID != -1:
print ("Modbus PID = ", TID)
break
# create 256 register address space for Modbus registers
# each register is 16 bits wide, we are using 32 bit Floats, so each value takes two registers
# registers thus must be addressed 0, 2, 4 etc...
# any registers read that have not had WITS data placed in them will read -777.1
register_address_space = bytearray(struct.pack(">f", -777.1)) * 256

wanted_record_items = [ [] ] * 100 # create empty list of lists
############################## WHAT WITS ITEMS DO WE WANT? ###############################
wanted_record_items[1] = [40,41]
wanted_record_items[66] = [8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]


host = socket.gethostname() #MODBUS_IP_ADDRESS
print ("MODBUS- Hostname:", host)
port = 502

while True: ### main outer loop.  Should only run to initially bind to the TCP port.
modbus_outer_loops += 1
hard_restart_required = False
print ("MODBUS- Creating modbus_socket")
modbus_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print ("MODBUS- Binding to",host,port)
try: # try to bind to the TCP port, will fail if network not up...
modbus_socket.bind((host,port))
except:
print ("MODBUS- Cannot bind to the Modbus TCP port! Is the network up?")
time.sleep(RESTART_WAIT)
continue

modbus_socket.listen(0)

while True: #############       Inner loop, deals with connections from client (MSI SBC)
modbus_inner_loops += 1
print ("MODBUS- Waiting for incoming connection on",host,port,"...")
client_socket,addr = modbus_socket.accept()
client_socket.settimeout(TCP_TIMEOUT)
print ("MODBUS- ",modbus_outer_loops,", ",modbus_inner_loops,", Incoming connection from %s" % str(addr))

################################ MAIN RESPONSE LOOP ######################################
# this loop reads from the WITS list
while True:
# retrieve the list from the queue
try:
record_item = wits_data_q.get(True, QUEUE_GET_TIMEOUT)
except:
pass # if there's nothing waiting in the queue, do nowt...
empty_queues += 1
else:
# if there is, update the registers...
# Read the wanted WITS data into the register space
#register = 0 not used (was a test signal)
#register_address_space[0:3] = bytearray(struct.pack(">f", sinewave_float))
#register += 2 # We use TWO 16 bit registers per float!
start_time = timer()
register = 2
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
register_address_space[register * 2:(register * 2)+3] = bytearray(struct.pack(">f", record_item[record][item] ))
register += 2

pass # end of loop what reads from WITS list
modbus_wits_reads += 1
read_time = ( timer() - start_time ) * 1000

try:
request = client_socket.recv(SOCKET_BUFFER)
except:
print ("MODBUS- Connection failed.")
print ("MODBUS- Closing socket and restarting listener.")
break
if not request:
print ("MODBUS- Connection established but no data received. (Connection closed by peer?)")
print ("MODBUS- Hard restarting. Killing server socket and restarting listener from scratch")
hard_restart_required = True
break

start_time = timer()

# Build the Modbus response
print ("MODBUS- -Request:"," ".join("%02x" % b for b in request))

transactionID0 = request[0]
transactionID1 = request[1]
protocol0 = request[2]
protocol1 = request[3]
unitID = request[6]
function_code = request[7]
start_register_hi_byte = request [8]
start_register_lo_byte = request[9]
number_of_registers_hi_byte = request[10]
number_of_registers_lo_byte = request[11]

function_code = 3 # function code 3 is all you get! take it or leave it!

start_register = start_register_lo_byte + (start_register_hi_byte * 256)
number_of_registers = number_of_registers_lo_byte + (start_register_hi_byte * 256)

byte_count = number_of_registers * 2 # registers are 16 bit
length = byte_count + 3 # data length plus, byte count, func code and unit ID
length_hi_byte = length >> 8
length_lo_byte = length & 255

response_list = [transactionID0, transactionID1, protocol0, protocol1, length_hi_byte, length_lo_byte, unitID, function_code, byte_count] # add all that shit together as a list

response_list += register_address_space[start_register * 2 : (start_register * 2) + (number_of_registers * 2)] # add the data onto the end of the response

response_bytes = bytes(response_list) # turn it into a byte string



print ("MODBUS- Response:"," ".join("%02x" % b for b in response_bytes))
client_socket.send(response_bytes)
response_time = ( timer() - start_time ) * 1000
modbus_requests_served += 1
print ("MODBUS- ",modbus_outer_loops,", ",modbus_inner_loops," - ",modbus_requests_served," requests served. ", modbus_wits_reads," WITS reads. ", empty_queues, " empty queues. Read time = %.3f" % read_time, " Response time = %.3f" % response_time, " PID = ", TID, sep="")
               
################# END OF MAIN MODBUS RESPONSE LOOP ####################################
# time.sleep(0.025)
print ("MODBUS- EXITING INNER LOOP-------------------------------")
client_socket.close()
if hard_restart_required:
hard_restart_required = False
break
time.sleep(MODBUS_RESTART_WAIT)
print ("MODBUS- EXITING OUTER LOOP---------------------")
modbus_socket.close()



##############################################################################
## END OF MODBUS SERVER
##############################################################################

I am going to put the WHAT ITEMS DO WE WANT bit inside the response loop, in case that is somehow growing with each iteration...
« Last Edit: February 15, 2018, 11:12:14 pm by Delta »
 

Offline Muxr

  • Super Contributor
  • ***
  • Posts: 1369
  • Country: us
Re: Python TCP server - CPU usage continually increasing over time
« Reply #11 on: February 15, 2018, 11:19:26 pm »
Where is:

Code: [Select]
record_item[record][item]
Coming from? First time I see a reference of record_item in your code.
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #12 on: February 15, 2018, 11:24:45 pm »
Where is:

Code: [Select]
record_item[record][item]
Coming from? First time I see a reference of record_item in your code.

Sorry, I have posted the whole code now.

(it is read from a queue, the other thread .put()s it there...)
Code: [Select]
################################ MAIN RESPONSE LOOP ######################################
# this loop reads from the WITS list
while True:
# retrieve the list from the queue
try:
record_item = wits_data_q.get(True, QUEUE_GET_TIMEOUT)
except:
pass # if there's nothing waiting in the queue, do nowt...
empty_queues += 1
else:
# if there is, update the registers...
 

Offline Muxr

  • Super Contributor
  • ***
  • Posts: 1369
  • Country: us
Re: Python TCP server - CPU usage continually increasing over time
« Reply #13 on: February 15, 2018, 11:38:02 pm »
It is still unclear to me what data this carries: record_item[record][item] this could be increasing somehow.

perhaps add a debugging line and do: print(len(bytearray(struct.pack(">f", record_item[record][item] ))))

Because I think you're always expecting it to be 3 and that may not be the case.

Also if I may have another comment. It is never a good idea to have catch all blind excepts. except:  Your except should be specific to the error you're handling for example except TypeError: or whatever error you're trying to handle. You might be getting other exceptions that you might be completely blind to, because your code excepts them all blindly.

edit: in your case for that queue try/expect it looks like you're waiting on Empty exception. So your expect should be. expect Queue.Empty:
« Last Edit: February 15, 2018, 11:45:48 pm by Muxr »
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #14 on: February 16, 2018, 12:43:10 am »
Here is the thread that produces the data (record_item).  Please forgive the shitness.
I will also post the part that actually writes to this list below.

Code: [Select]
def run(self):
WITS_IP_ADDRESS = "192.168.31.97" # IP address of Serial Server
WITS_TCP_PORT = 10001
TCP_TIMEOUT = 10
SOCKET_BUFFER = 1024
MODBUS_RESTART_WAIT = 0
RESTART_WAIT = 3
START_DELIMITER = "&&"
END_DELIMITER = "!!"
MAX_BUFFER_SIZE = 65000
MIN_BUFFER_SIZE = 1000 # not really the minimum as such, but how much is kept when it's emptied
WITS_frames_received = 0
exceptions = 0
WITS_outer_loops = 0
WITS_inner_loops = 0

full_queues = 0
QUEUE_PUT_TIMEOUT = 0.5
libc = 'libc.so.6'
for cmd in (186, 224, 178):
TID = ctypes.CDLL(libc).syscall(cmd)
if TID != -1:
print ("wits PID = ", TID)
break

######################### START OF MAIN LOOP ##################
while True:
print ("WITS- Setting all variables to -666.1 to indicate lack of incoming stream")
record_item = [ [-666.1] * 100 ] * 100
WITS_outer_loops +=1
#create socket
try:
WITS_socket.close() # try to close in case ocht left open, ken?
except:
print ("WITS- Socket didna need closing!")
WITS_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
hostname = socket.gethostname()
WITS_socket.settimeout(TCP_TIMEOUT)
print ("WITS- Connecting to",WITS_IP_ADDRESS,"port",WITS_TCP_PORT)
try:
WITS_socket.connect((WITS_IP_ADDRESS, WITS_TCP_PORT))
except:
print ("WITS- Could not connect!")
print ("WITS- Waiting",RESTART_WAIT,"seconds")
time.sleep(RESTART_WAIT)
continue

print ("WITS- Receiving a chunk of data...")
input_buffer = "" # buffer is a STR type
WITS_frame = ""
while True:
WITS_inner_loops += 1
try:
chunk = WITS_socket.recv(SOCKET_BUFFER)
except:
print ("WITS- ",WITS_outer_loops,", ",WITS_inner_loops," - Could not receive!", sep="")
time.sleep(RESTART_WAIT)
break
if not chunk: # if the recv() call returns empty, GTF oot of here!
print ("WITS- ",WITS_outer_loops,", ",WITS_inner_loops," - recv() returned empty!", sep="")
time.sleep(RESTART_WAIT)
break
input_buffer = input_buffer + chunk.decode() # need to decode from a BYTE stream to the default STR type
input_buffer_size = len(input_buffer)
while input_buffer.find(START_DELIMITER) >= 0 and input_buffer.find(END_DELIMITER,input_buffer.find(START_DELIMITER)) >= 0: # keep checking the buffer for WITS shit
start_delimiter_position = input_buffer.find(START_DELIMITER)
end_delimiter_position = input_buffer.find(END_DELIMITER,start_delimiter_position)
if start_delimiter_position >= 0:
# print ("WITS- found",START_DELIMITER,"at buffer position",start_delimiter_position)
# print ("found",END_DELIMITER,"at buffer position",end_delimiter_position,"ken?")

if end_delimiter_position >= start_delimiter_position:
# print ("WITS- found",END_DELIMITER,"at buffer position",end_delimiter_position)
end_delimiter_position += 2 # add 2 to the end position as it is a double doofer
WITS_frames_received += 1
# print ("WITS- Creating WITS frame",WITS_frames_received,"from from buffer positions",start_delimiter_position,"to",end_delimiter_position)
WITS_frame = input_buffer[start_delimiter_position:end_delimiter_position]
# convert frame into individual lines
WITS_lines = WITS_frame.splitlines()
line_num = 0

# this is the loop that writes into the WITS record_item list
for item in WITS_lines:
line = WITS_lines[line_num]
# print ("Line",str(line_num),"--",line)
#extract data into variables
if line[:4].isdigit() and len(line) > 4: # check the line is at least 4 chars long and first 4 are digits

record_num = int(line[:2]) # get record number
try:
item_num = int(line[2:4])
except ValueError:
print ("WITS- --Exception! ValueError thrown extracting item number from line",line_num)
exceptions += 1
if is_float(line[4:]): # if the remainder of the line is a valid float
record_item[record_num][item_num] = float(line[4:])
print ("Variable for record ",record_num,", item ",item_num," has been assigned a value of ",record_item[record_num][item_num],sep="")

line_num += 1
if line_num > 100: # sanity check
print ("WITS- Line number is over 100!  Breaking from loop!")
break
pass # this is the exit from the WITS record_item loop
# send the new WITS data to the queue
try:
wits_data_q.put(record_item, True, QUEUE_PUT_TIMEOUT)
except queue.Full:
pass # if the queue already has an item in it, just leave it
full_queues += 1

input_buffer = input_buffer[end_delimiter_position + 1:] # empty buffer prior to end of frame
# print ("WITS- Input buffer size is now",len(input_buffer))
print ("WITS- ",WITS_outer_loops,", ",WITS_inner_loops," - ",WITS_frames_received," frames received, buffer = ",len(input_buffer), ". ", full_queues, " full queues. PID = ", TID, sep="")
# print (modbus_requests_served,"Modbus requests served")
### map data to Modbus registers....######################################
pass # not sure where we are here?!!
if len(input_buffer) > MAX_BUFFER_SIZE:
input_buffer = input_buffer[MAX_BUFFER_SIZE - MIN_BUFFER_SIZE:]
print ("WITS- BUFFER DITCHED!")

print ("WITS- Aboot to close the WITS socket")
WITS_socket.close()
print ("WITS- Waiting",RESTART_WAIT,"seconds...")
time.sleep(RESTART_WAIT)
########################################################
## End of WITS client ##################################
########################################################

The list is written to by:
Code: [Select]
# this is the loop that writes into the WITS record_item list
for item in WITS_lines:
line = WITS_lines[line_num]
# print ("Line",str(line_num),"--",line)
#extract data into variables
if line[:4].isdigit() and len(line) > 4: # check the line is at least 4 chars long and first 4 are digits

record_num = int(line[:2]) # get record number
try:
item_num = int(line[2:4])
except ValueError:
print ("WITS- --Exception! ValueError thrown extracting item number from line",line_num)
exceptions += 1
if is_float(line[4:]): # if the remainder of the line is a valid float
record_item[record_num][item_num] = float(line[4:])
print ("Variable for record ",record_num,", item ",item_num," has been assigned a value of ",record_item[record_num][item_num],sep="")

line_num += 1
if line_num > 100: # sanity check
print ("WITS- Line number is over 100!  Breaking from loop!")
break
pass # this is the exit from the WITS record_item loop
# send the new WITS data to the queue
try:
wits_data_q.put(record_item, True, QUEUE_PUT_TIMEOUT)
except queue.Full:
pass # if the queue already has an item in it, just leave it
full_queues += 1

Good shout about the exceptions - I will add the queue.Empty to it.

I will also check the length of the record_item list that is getting passed to the Modbus server thread....

Thanks for your help mate.
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #15 on: February 16, 2018, 01:24:12 am »
Right, I have added
Code: [Select]
pass # this is the exit from the WITS record_item loop
length_record_item = sys.getsizeof(record_item)
# send the new WITS data to the queue
try:
wits_data_q.put(record_item, True, QUEUE_PUT_TIMEOUT)
To the producer thread to see the size of the
Code: [Select]
record_item object that is placed into the queue.

And
Code: [Select]
register_address_space[register * 2:(register * 2)+3] = bytearray(struct.pack(">f", record_item[record][item] ))
size_of_item = len(bytearray(struct.pack(">f",record_item[record][item])))
if size_of_item > largest_item:
largest_item = size_of_item
register += 2
to the thread that reads each item from this object.

length_record_item is always 436.
The largest item size is 4 bytes, just as expected, yet the execution time is increasing before my very eyes...  :( :'(
 

Offline Muxr

  • Super Contributor
  • ***
  • Posts: 1369
  • Country: us
Re: Python TCP server - CPU usage continually increasing over time
« Reply #16 on: February 16, 2018, 02:25:52 am »
Still analyzing the code.. (it's not an easiest structure to follow, I am sure you realize).. but right of the bat something sticks out.

You might have python's pass statement confused. pass in python is no-op. It doesn't do anything. It's really only useful for for closing empty blocks. Since Python's blocks can't really be empty, for instance having an empty function doesn't work because the spacing is recognized as a begining of the block.

Code: [Select]
def some_function():

## above would throw an error
which is where pass comes in

Code: [Select]
def some_function():
    pass


You might be logically confusing it for break which can exit an existing loop.
 

Offline Muxr

  • Super Contributor
  • ***
  • Posts: 1369
  • Country: us
Re: Python TCP server - CPU usage continually increasing over time
« Reply #17 on: February 16, 2018, 02:55:31 am »
I think your slice math might be wrong:

Code: [Select]
[register * 2:(register * 2)+3]
This doesn't look right to me.. again not entirely sure of your intentions. But I can tell you, this is not the reason why your code is running slower. I just ran this in interactive iPython:

Code: [Select]
# initialized wanted_record_items like you have it
In [10]: wanted_record_items = [ [] ] * 100
In [11]: wanted_record_items[1] = [40,41]
In [12]: wanted_record_items[66] = [8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]

# and then just had your same loop print the line in the loop the code should be iterating over with appropriate indices (slice math)
In [25]: for record in list(range(100)):
    ...:     for item in list(wanted_record_items[record]):
    ...:         print("reguster_address_space[{}:{}] = bytearray(struct.pack(\">f\", record_item[{}][{}]))
    ...: ".format(register * 2, (register * 2) + 3, record, item))
    ...:         register += 2

# result:
reguster_address_space[4:7] = bytearray(struct.pack(">f", record_item[1][40]))
reguster_address_space[8:11] = bytearray(struct.pack(">f", record_item[1][41]))
reguster_address_space[12:15] = bytearray(struct.pack(">f", record_item[66][8]))
reguster_address_space[16:19] = bytearray(struct.pack(">f", record_item[66][9]))
reguster_address_space[20:23] = bytearray(struct.pack(">f", record_item[66][10]))
reguster_address_space[24:27] = bytearray(struct.pack(">f", record_item[66][11]))
reguster_address_space[28:31] = bytearray(struct.pack(">f", record_item[66][12]))
reguster_address_space[32:35] = bytearray(struct.pack(">f", record_item[66][13]))
reguster_address_space[36:39] = bytearray(struct.pack(">f", record_item[66][14]))
reguster_address_space[40:43] = bytearray(struct.pack(">f", record_item[66][15]))
reguster_address_space[44:47] = bytearray(struct.pack(">f", record_item[66][16]))
reguster_address_space[48:51] = bytearray(struct.pack(">f", record_item[66][17]))
reguster_address_space[52:55] = bytearray(struct.pack(">f", record_item[66][18]))
reguster_address_space[56:59] = bytearray(struct.pack(">f", record_item[66][19]))
reguster_address_space[60:63] = bytearray(struct.pack(">f", record_item[66][20]))
reguster_address_space[64:67] = bytearray(struct.pack(">f", record_item[66][21]))
reguster_address_space[68:71] = bytearray(struct.pack(">f", record_item[66][22]))
reguster_address_space[72:75] = bytearray(struct.pack(">f", record_item[66][23]))
reguster_address_space[76:79] = bytearray(struct.pack(">f", record_item[66][24]))
reguster_address_space[80:83] = bytearray(struct.pack(">f", record_item[66][25]))
reguster_address_space[84:87] = bytearray(struct.pack(">f", record_item[66][26]))
reguster_address_space[88:91] = bytearray(struct.pack(">f", record_item[66][27]))
reguster_address_space[92:95] = bytearray(struct.pack(">f", record_item[66][28]))
reguster_address_space[96:99] = bytearray(struct.pack(">f", record_item[66][29]))
reguster_address_space[100:103] = bytearray(struct.pack(">f", record_item[66][30]))
reguster_address_space[104:107] = bytearray(struct.pack(">f", record_item[66][31]))
Does that look right?

It ran instantly on my machine.. so the reason for the slowdown is something else. I suspect the way your server (it really looks like a client to me) code is setup.

edit: excuse the typo in the list's name.. but you get the point.. there is nothing functionally wrong with that loop and it shouldn't be causing slow downs in my opinion.
« Last Edit: February 16, 2018, 03:04:53 am by Muxr »
 
The following users thanked this post: Delta

Offline Muxr

  • Super Contributor
  • ***
  • Posts: 1369
  • Country: us
Re: Python TCP server - CPU usage continually increasing over time
« Reply #18 on: February 16, 2018, 02:59:13 am »
My recommendation is to rewrite the code to not rely on so many nesting loops. This is generally really difficult to troubleshoot and it also makes it much harder to test units of your code for sanity.

I would go and break every single nested loop, into its own function. This way you can test them individually and have a much better picture of what's happening.

There should really be only one main loop.. your server. Everything else should be spawned from it.
« Last Edit: February 16, 2018, 03:13:25 am by Muxr »
 

Offline bson

  • Supporter
  • ****
  • Posts: 2269
  • Country: us
Re: Python TCP server - CPU usage continually increasing over time
« Reply #19 on: February 16, 2018, 01:53:29 pm »
Is there a reason you can't use a simple generator pattern for this, using yield?  Having to use threads and a queue just makes it unnecessarily complicated - remember that python is inherently single threaded.  Your time measurement isn't execution time but wall time, and there may be something else spinning on the single underlying bound thread - and so your code might not be slow, it might be starved.

https://wiki.python.org/moin/Generators
 

Offline paulca

  • Super Contributor
  • ***
  • Posts: 4031
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #20 on: February 16, 2018, 02:35:06 pm »
I promised a quick sample of using the SocketServer library which should tidy things up.

A small excerpt:
Code: [Select]
class ProbeReceiverHandler(socketserver.BaseRequestHandler):
        def handle(self):
            data = self.request[0].strip()
            socket = self.request[1]
            print("Recieved from {}:".format(self.client_address[0]))
            data_str=data.decode("utf-8")
            print(data_str+"\n")
            temps = data_str.split(",")
            for temp in temps:
                name = temp.split(":")[0]
                value = temp.split(":")[1]
                latest_temps[name] = float(value)

class ProbeReceiverThread(socketserver.ThreadingMixIn, socketserver.UDPServer):
        pass


class DisplaySenderHandler(socketserver.BaseRequestHandler):
        def handle(self):
            message = ""
            sep = ""
            for key, value in latest_temps.items():
                message+="{0!s}{1!s}:{2!s}".format(sep,str(key),str(value))
                sep=","
            self.request.sendall(bytes(message, "utf-8"))
            print("Sent to {0!s}:{1!s}".format(self.client_address[0], message))

class DisplaySenderThread(socketserver.ThreadingMixIn, socketserver.TCPServer):
        pass


if __name__ == "__main__":
    HOST, P_PORT, D_PORT = "0.0.0.0", 9999, 9998

    probe_server = ProbeReceiverThread((HOST, P_PORT), ProbeReceiverHandler)
    display_server = DisplaySenderThread((HOST, D_PORT), DisplaySenderHandler)
    try:
        th = threading.Thread(target=probe_server.serve_forever)
        th.start()

        th = threading.Thread(target=display_server.serve_forever)
        th.start()
"What could possibly go wrong?"
Current Open Projects:  STM32F411RE+ESP32+TFT for home IoT (NoT) projects.  Child's advent xmas countdown toy.  Digital audio routing board.
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #21 on: February 16, 2018, 10:03:14 pm »
Thanks for all the advice, I know my code is all over the place, and I will certainly take on board the tips given.

However I am still tearing my hair out as to why on earth this simple block takes ever longer to run.

I have stripped out the troublesome block and am running the following test script on a laptop with an i5-3380M.

Code: [Select]
#!/usr/bin/python3

import struct
import time
import math
from timeit import default_timer as timer

DELAY = 0.001
PRINT_ITERS = 1000
iterations = 0
modbus_wits_reads = 0

record_item = [ [-666.1] * 100 ] * 100 # dummy data

# create 256 register address space for Modbus registers
# each register is 16 bits wide, we are using 32 bit Floats, so each value takes two registers
# registers thus must be addressed 0, 2, 4 etc...
# any registers read that have not had WITS data placed in them will read -777.1
register_address_space = bytearray(struct.pack(">f", -777.1)) * 256
wanted_record_items = [ [] ] * 100 # create empty list of lists
############################# WHAT WITS ITEMS DO WE WANT? ###############################
wanted_record_items[1] = [40,41]
wanted_record_items[66] = [8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]


################################ MAIN RESPONSE LOOP ######################################

while True:
start_time = timer()
register = 2
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
register_address_space[register * 2:(register * 2)+3] = bytearray(struct.pack(">f", record_item[record][item] ))
register += 2
pass # end of loop what reads from WITS list
modbus_wits_reads += 1
iterations += 1
read_time = ( timer() - start_time ) * 1000
if iterations >= PRINT_ITERS:
iterations = 0
print (modbus_wits_reads, "loops. Read time = %.3f" % read_time)
time.sleep(DELAY)

After 1000 loops the block takes .345ms to execute, then after 129000 loops it is up to 8ms!
(I understand that this is not an accurate measurement, as it is "wall time")

output:
Code: [Select]
1000 loops. Read time = 0.345
2000 loops. Read time = 0.334
3000 loops. Read time = 0.433
4000 loops. Read time = 0.424
5000 loops. Read time = 0.518
6000 loops. Read time = 0.503
7000 loops. Read time = 0.702
8000 loops. Read time = 0.504
9000 loops. Read time = 0.585
10000 loops. Read time = 0.597
11000 loops. Read time = 0.644
12000 loops. Read time = 0.825
13000 loops. Read time = 0.691
14000 loops. Read time = 0.926
15000 loops. Read time = 1.001
16000 loops. Read time = 0.823
17000 loops. Read time = 0.781
18000 loops. Read time = 0.861
19000 loops. Read time = 0.929
20000 loops. Read time = 1.086
21000 loops. Read time = 0.957
22000 loops. Read time = 0.940
23000 loops. Read time = 0.943
24000 loops. Read time = 0.981
25000 loops. Read time = 1.114
26000 loops. Read time = 1.073
27000 loops. Read time = 1.101
28000 loops. Read time = 1.208
29000 loops. Read time = 1.147
30000 loops. Read time = 1.045
31000 loops. Read time = 1.164
32000 loops. Read time = 1.292
33000 loops. Read time = 1.328
34000 loops. Read time = 1.276
35000 loops. Read time = 1.197
36000 loops. Read time = 1.459
37000 loops. Read time = 1.506
38000 loops. Read time = 1.468
39000 loops. Read time = 1.529
40000 loops. Read time = 1.527
41000 loops. Read time = 1.629
42000 loops. Read time = 1.583
43000 loops. Read time = 1.658
44000 loops. Read time = 1.686
45000 loops. Read time = 1.602
46000 loops. Read time = 1.667
47000 loops. Read time = 1.642
48000 loops. Read time = 1.720
49000 loops. Read time = 1.557
50000 loops. Read time = 1.808
51000 loops. Read time = 1.864
52000 loops. Read time = 1.701
53000 loops. Read time = 1.731
54000 loops. Read time = 1.730
55000 loops. Read time = 1.847
56000 loops. Read time = 1.996
57000 loops. Read time = 1.812
58000 loops. Read time = 2.087
59000 loops. Read time = 2.089
60000 loops. Read time = 2.108
61000 loops. Read time = 2.309
62000 loops. Read time = 1.985
63000 loops. Read time = 2.180
64000 loops. Read time = 2.079
65000 loops. Read time = 2.236
66000 loops. Read time = 2.227
67000 loops. Read time = 2.240
68000 loops. Read time = 2.139
69000 loops. Read time = 2.303
70000 loops. Read time = 2.420
71000 loops. Read time = 2.475
72000 loops. Read time = 2.457
73000 loops. Read time = 2.546
74000 loops. Read time = 2.553
75000 loops. Read time = 2.613
76000 loops. Read time = 2.638
77000 loops. Read time = 2.630
78000 loops. Read time = 2.718
79000 loops. Read time = 2.837
80000 loops. Read time = 2.723
81000 loops. Read time = 2.842
82000 loops. Read time = 2.808
83000 loops. Read time = 2.962
84000 loops. Read time = 6.680
85000 loops. Read time = 3.183
86000 loops. Read time = 2.968
87000 loops. Read time = 3.147
88000 loops. Read time = 2.886
89000 loops. Read time = 3.349
90000 loops. Read time = 5.483
91000 loops. Read time = 3.445
92000 loops. Read time = 3.433
93000 loops. Read time = 3.325
94000 loops. Read time = 3.696
95000 loops. Read time = 3.325
96000 loops. Read time = 3.333
97000 loops. Read time = 3.901
98000 loops. Read time = 3.864
99000 loops. Read time = 3.511
100000 loops. Read time = 3.585
101000 loops. Read time = 3.889
102000 loops. Read time = 4.086
103000 loops. Read time = 4.135
104000 loops. Read time = 3.977
105000 loops. Read time = 4.368
106000 loops. Read time = 4.422
107000 loops. Read time = 4.421
108000 loops. Read time = 4.402
109000 loops. Read time = 5.384
110000 loops. Read time = 4.726
111000 loops. Read time = 4.843
112000 loops. Read time = 5.086
113000 loops. Read time = 5.289
114000 loops. Read time = 5.483
115000 loops. Read time = 7.556
116000 loops. Read time = 5.696
117000 loops. Read time = 5.452
118000 loops. Read time = 6.211
119000 loops. Read time = 5.621
120000 loops. Read time = 5.948
121000 loops. Read time = 5.874
122000 loops. Read time = 6.266
123000 loops. Read time = 6.282
124000 loops. Read time = 6.714
125000 loops. Read time = 8.868
126000 loops. Read time = 6.873
127000 loops. Read time = 6.688
128000 loops. Read time = 7.426
129000 loops. Read time = 8.047

What on earth is going on?  NOTHING changes with each loop!  Is there some Python overhead increasing with each iteration?  Is there some way to "reset" this every X iterations?  Garbage collection?

I am really confused!
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #22 on: February 16, 2018, 10:45:34 pm »
FOUND IT!

register_address_space is growing!

I added a r_a_s_size = sys.getsizeof(register_address_space) to my test script, so we now have

Code: [Select]
while True:
start_time = timer()
register = 2
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
start_address = register * 2
end_address = start_address + 3
register_address_space[start_address:end_address] = bytearray(struct.pack(">f", record_item[record][item] ))
register += 2
pass # end of loop what reads from WITS list
r_a_s_size = sys.getsizeof(register_address_space)
modbus_wits_reads += 1
iterations += 1
read_time = ( timer() - start_time ) * 1000
if iterations >= PRINT_ITERS:
iterations = 0
print (modbus_wits_reads, "loops. Read time = %.3f" % read_time, "Address space size =", r_a_s_size)
time.sleep(DELAY)

And the output it:
Code: [Select]
1000 loops. Read time = 0.076 Address space size = 28943
2000 loops. Read time = 0.099 Address space size = 58663
3000 loops. Read time = 0.123 Address space size = 83521
4000 loops. Read time = 0.137 Address space size = 105704
5000 loops. Read time = 0.159 Address space size = 133779
6000 loops. Read time = 0.181 Address space size = 169311
7000 loops. Read time = 0.202 Address space size = 190473
8000 loops. Read time = 0.224 Address space size = 214281
9000 loops. Read time = 0.247 Address space size = 241065
10000 loops. Read time = 0.271 Address space size = 271197
11000 loops. Read time = 0.297 Address space size = 305095
12000 loops. Read time = 0.312 Address space size = 343230
13000 loops. Read time = 0.332 Address space size = 343230
14000 loops. Read time = 0.446 Address space size = 386132
15000 loops. Read time = 0.375 Address space size = 434397
16000 loops. Read time = 0.411 Address space size = 434397
17000 loops. Read time = 0.420 Address space size = 488695
18000 loops. Read time = 0.444 Address space size = 488695
19000 loops. Read time = 0.465 Address space size = 549780
20000 loops. Read time = 0.489 Address space size = 549780
21000 loops. Read time = 0.511 Address space size = 549780
22000 loops. Read time = 0.553 Address space size = 618501
23000 loops. Read time = 0.563 Address space size = 618501
24000 loops. Read time = 0.596 Address space size = 695812
25000 loops. Read time = 0.806 Address space size = 695812
26000 loops. Read time = 0.807 Address space size = 695812
27000 loops. Read time = 0.852 Address space size = 782787
28000 loops. Read time = 0.696 Address space size = 782787
29000 loops. Read time = 0.722 Address space size = 782787
30000 loops. Read time = 0.770 Address space size = 782787
31000 loops. Read time = 0.772 Address space size = 880634
32000 loops. Read time = 0.819 Address space size = 880634
33000 loops. Read time = 0.841 Address space size = 880634

 :wtf:
 

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #23 on: February 16, 2018, 10:59:32 pm »
As a workaround I am re-initialising(?) the register_address_space object(?) by putting the statement
Code: [Select]
register_address_space = bytearray(struct.pack(">f", -777.1)) * 256 inside the loop.

Code: [Select]
while True:
start_time = timer()
register_address_space = bytearray(struct.pack(">f", -777.1)) * 256
register = 2
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
start_address = register * 2
end_address = start_address + 3
register_address_space[start_address:end_address] = bytearray(struct.pack(">f", record_item[record][item] ))
register += 2
pass # end of loop what reads from WITS list
r_a_s_size = sys.getsizeof(register_address_space)
modbus_wits_reads += 1
iterations += 1
read_time = ( timer() - start_time ) * 1000
if iterations >= PRINT_ITERS:
iterations = 0
print (modbus_wits_reads, "loops. Read time = %.3f" % read_time, "Address space size =", r_a_s_size)
time.sleep(DELAY)


This seems to have done the trick, but why is this necessary?
 
The following users thanked this post: thm_w

Offline DeltaTopic starter

  • Super Contributor
  • ***
  • Posts: 1221
  • Country: gb
Re: Python TCP server - CPU usage continually increasing over time
« Reply #24 on: February 17, 2018, 03:16:34 am »
So something weird is going on with my bytearray object.

I use
Code: [Select]
test = bytearray(struct.pack(">f", -777.1)) * 2to create an object with space for 8 raw bytes (and fill them with two 32bit floats)

and thought that
Code: [Select]
test[0:3] = bytearray(struct.pack(">f", 123.456 ))would simply overwrite the first four bytes with a new 32bit float.

However...
Code: [Select]
>>> test = bytearray(struct.pack(">f", -777.1)) * 2
>>> print (test)
bytearray(b'\xc4BFf\xc4BFf')
>>> print (sys.getsizeof(test))
65
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (test)
bytearray(b'B\xf6\xe9yf\xc4BFf')
>>> print (sys.getsizeof(test))
72
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (test)
bytearray(b'B\xf6\xe9yyf\xc4BFf')
>>> print (sys.getsizeof(test))
72
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (test)
bytearray(b'B\xf6\xe9yyyf\xc4BFf')
>>> print (sys.getsizeof(test))
72
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (test)
bytearray(b'B\xf6\xe9yyyyf\xc4BFf')
>>> print (sys.getsizeof(test))
72
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (test)
bytearray(b'B\xf6\xe9yyyyyf\xc4BFf')
>>> print (sys.getsizeof(test))
72
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (test)
bytearray(b'B\xf6\xe9yyyyyyf\xc4BFf')
>>> print (sys.getsizeof(test))
72
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> test[0:3] = bytearray(struct.pack(">f", 123.456 ))
>>> print (sys.getsizeof(test))
89
>>> print (test)
bytearray(b'B\xf6\xe9yyyyyyyyyyyyyyyyyyf\xc4BFf')
>>>

Hmmmmmmm!  Have I got the wrong end of this stick here, or is there something weird going on?

(I should add that the data retrieved from this "register_address_space" in my script IS correct - the data that the Modbus client receives from my script is good)
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf