kirr has uploaded this change for review.

View Change

trx_toolkit/_fake_trx: Optimize IO loop fd polling

As can be seen from http://navytux.spb.ru/~kirr/osmo/fake_trx/pyx-base.html (Runner_5loop)
the system spends more in py select wrapper compared to select system
call itself. And also the wrapper releases/reacquires gil, which,
as Iaa675c95059ec8ccfad667f69984d5a7f608c249 (trx_toolkit/clck_gen: Don't
use threads because Python GIL is latency killer) shows, can have
dramatic effect. It is also known that select inside the kernel is doing
useless work at every call by registering/deregistering each fd every
time.

-> Avoid all that overhead by switching to epoll and doing epoll_wait
ourselves and without releasing/reacquiring the gil. We can do that
because fake_trx is single-threaded and because clck_gen._timerfd is
setup to do ~ 200 Hz regular wakeup.

Change-Id: I748810871601178cc97bcdaba41419949078c29d
---
M src/target/trx_toolkit/_fake_trx.pyx
1 file changed, 111 insertions(+), 18 deletions(-)

git pull ssh://gerrit.osmocom.org:29418/osmocom-bb refs/changes/64/40064/1
diff --git a/src/target/trx_toolkit/_fake_trx.pyx b/src/target/trx_toolkit/_fake_trx.pyx
index f4cf7f6..e540e14 100644
--- a/src/target/trx_toolkit/_fake_trx.pyx
+++ b/src/target/trx_toolkit/_fake_trx.pyx
@@ -25,6 +25,7 @@
from _clck_gen cimport CLCKGen
from transceiver import Transceiver
from data_msg import Modulation
+from udp_link cimport _raise_oserr
from gsm_shared import *


@@ -373,6 +374,30 @@
return None


+# ----------------------------------------
+
+from libc.stdint cimport int64_t, uint32_t, uint64_t
+from libc.stdlib cimport calloc, free
+
+cdef extern from "<sys/epoll.h>":
+ union epoll_data_t:
+ void *ptr
+ int fd
+ uint32_t u32
+ uint64_t u64
+
+ struct epoll_event:
+ uint32_t events
+ epoll_data_t data
+
+ enum:
+ EPOLL_CTL_ADD
+ EPOLLIN
+
+ int epoll_ctl(int epfd, int op, int fd, epoll_event *event)
+ int epoll_wait(int epfd, epoll_event *events, int maxevents, int timeout)
+
+
# Runner organizes execution of several FakeTRX instances with common clock.
cdef class Runner:
cdef CLCKGen clck_gen
@@ -396,26 +421,94 @@

# loops runs IO loop on specified CLCKGen and TRXes forever.
def loop(self):
- sock_list = [self.clck_gen._timerfd]
+ epoll = select.epoll()
+ cdef int epoll_fd = epoll.fileno()
+
+ cdef epoll_event ee
+ ee = _heventdef(_Handler(_RxTimer, None))
+ xepoll_ctl(epoll_fd, EPOLL_CTL_ADD, self.clck_gen._timerfd, &ee)
+
for trx in self.trx_list:
- sock_list.append(trx.ctrl_if.sock)
- sock_list.append(trx.data_if.sock)
+ ee = _heventdef(_Handler(_RxTRXCtrl, trx))
+ xepoll_ctl(epoll_fd, EPOLL_CTL_ADD, trx.ctrl_if.sock.fileno(), &ee)

- # Enter main loop
- while True:
- # Wait until we get any data on any socket
- r_event, _, _ = select.select(sock_list, [], [])
+ ee = _heventdef(_Handler(_RxTRXData, trx))
+ xepoll_ctl(epoll_fd, EPOLL_CTL_ADD, trx.data_if.sock.fileno(), &ee)

- # clock is priority
- if self.clck_gen._timerfd in r_event:
- self.clck_gen._tick()

- # Iterate over all transceivers
- for trx in self.trx_list:
- # DATA interface
- if trx.data_if.sock in r_event:
- trx.recv_data_msg()
+ cdef int maxevents = 1 + 2*len(self.trx_list)
+ cdef epoll_event *events = <epoll_event*>calloc(maxevents, sizeof(events[0]))
+ if events == NULL:
+ raise MemoryError()

- # CTRL interface
- if trx.ctrl_if.sock in r_event:
- trx.ctrl_if.handle_rx()
+ try:
+
+ # Enter main loop
+ while True:
+ # Wait until we get any data on any socket
+ #
+ # NOTE we do not release/reacquire gil to save us from gil ping-pong performance penalty.
+ # we can do that because fake_trx is single-threaded and because clck_gen._timerfd
+ # is setup to do ~ 200 Hz regular wakeup.
+ n = epoll_wait(epoll_fd, &events[0], maxevents, -1)
+ if n == -1:
+ _raise_oserr()
+
+ # clock is priority
+ for i in range(n):
+ h = <_Handler>events[i].data.ptr
+ if h.kind == _RxTimer:
+ self.clck_gen._tick()
+
+ # data / ctrl
+ for i in range(n):
+ h = <_Handler>events[i].data.ptr
+ if h.kind == _RxTRXData:
+ h.trx.recv_data_msg()
+
+ elif h.kind == _RxTRXCtrl:
+ h.trx.ctrl_if.handle_rx()
+
+ elif h.kind == _RxTimer:
+ pass
+
+ else:
+ raise AssertionError('bug')
+
+ finally:
+ free(events)
+
+
+# _Handler represents how POLLIN event on an fd should be handled.
+cdef class _Handler:
+ cdef _HandlerKind kind
+ cdef object trx # Tranceiver | None
+
+ def __cinit__(self, _HandlerKind kind, trx):
+ self.kind = kind
+ self.trx = trx
+ _hkeepalive.append(self) # so that _heventdef works without special care
+
+cdef enum _HandlerKind:
+ _RxTimer
+ _RxTRXCtrl
+ _RxTRXData
+
+cdef list _hkeepalive = []
+
+
+# _heventdef returns epoll_event suitable for registering that links to specified _Handler.
+#
+# NOTE the caller must make sure to keep handler object alive during epoll loop run.
+cdef epoll_event _heventdef(_Handler h):
+ cdef epoll_event e
+ e.events = EPOLLIN
+ e.data.ptr = <void*>h
+ return e
+
+
+# xepoll_ctl invokes epoll_ctl and raises exception on error.
+cdef xepoll_ctl(int efd, int op, int fd, epoll_event* event):
+ err = epoll_ctl(efd, op, fd, event)
+ if err == -1:
+ _raise_oserr()

To view, visit change 40064. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-MessageType: newchange
Gerrit-Project: osmocom-bb
Gerrit-Branch: master
Gerrit-Change-Id: I748810871601178cc97bcdaba41419949078c29d
Gerrit-Change-Number: 40064
Gerrit-PatchSet: 1
Gerrit-Owner: kirr <kirr@nexedi.com>
Gerrit-CC: fixeria <vyanitskiy@sysmocom.de>
Gerrit-CC: osmith <osmith@sysmocom.de>
Gerrit-CC: pespin <pespin@sysmocom.de>