Hi,
here is a memo describing the debugging session we did yesterday with
Julien. I have added TODO each time I think we can do better.
MINA analyze when processing one single message. The server receives
"hello" and should return "HELLO".
- We start in the main IoProcessor loop, after a select() returning 1 on
a channel set with OP_READ, as we have just received the message.
- the process() method is called. It does two things :
1) if the session is readable and not suspended, read the channel
2) is the session is writable and not suspended, add the session to the
queue of session ready for flush, if not already there.
TODO : at this point, we have no idea if the session has some pending
writes. Check if the OP_WRTE flag has been set on the selectionKey
- in the read() method, we allocate a buffer with the size configured
for the session. Usually, it's way to big if you just have hundreds of
bytes to read, and if you set the config to 65000…
TODO : use smaller buffers
- Then we call the chain's messageReceived() method
- In the ProtocolCodecFilter, we grab the decoder instance, which is
stored in the session's Attributes
TODO : There is no good reason to store the instance in the attributes.
It should be part of the session parameters.
- then we create a new instance of a decoderOut.
TODO : The only reason we have such an object created here is that we
need to create a queue to store the result of the decoding.
- We now decode the incoming data, until the buffer is empty. We may
have more than one message in the buffer, so each one of them is decoded
and enqueued in the encoderOut queue.
- The doDecode() method is called. This is the user provided code. The
decoded message is enqueued, and we do that until the doDecode() method
returns false (meaning we don't have anymore message to decode)
- We call the encodedOut.flush() method to go up in the chain
TODO : it should be done directly here
- For each decoded message in the queue, call the next filter
messageReceived() event
TODO : we went through the tail filter, which is updating some stats
used by the idle session handler, but this should be done in another
place. Everyone does not want to deal with idle sessions … My be an
IdleFilter could help ?
- In the handler, we process the message, and write the response calling
session.write( response )
- The session.write() method create a writeFuture and a WriteRequest
object containing the response, the recipient's address and the future
TODO : this WriteFuture is never used.
- We go back to the chain in reverse order, firing the FilterWrite method
TODO : This should be a separate chain
- We get the encoded and we create an encoderOut instance, containing a
queue
TODO : here, unless the encoder creates more than one buffer to be sent,
there is no need to create a queue. We can also call the encoder, get
back the result and flush it, doing so receptively until all the encoded
pieces has been generated (but I feel like we better get back a set of
buffers instead as a response. No need of a queue)
- The user's provided encoder method is called, the encoded buffer is
enqueued. It can still be a plain Message, if we want to go though
another encoder
- At this point, we call the flushWithoutFuture() method which loop on
the encoderOut queue to send all the encoded buffers
- for each encoded message in the queue, we create an
EncodedWriteRequest encapsulating the encoded buffer, and we go down in
the chain
- The WriteRequest is enqueued in the session WriteRequestQueue, and if
the session is not suspended for write, we call the processor.flus() method
TODO : there is no reason to call this method now. Also the
session.getProcessor() method ask the processor pool for the session's
processor, which is useless, as each session is attached to a single
IoProtocol. We must store not the pool, but the processor
- If the session is not already in the queue of session waiting to be
flushed, we add it into this queue, then we wake up the selector
TODO : Waking up the selector at this point is questionable. It's
probably totally useless.
- Ok, we go back, and call the filterWrite() method once again in the
protocolCodecFilter, with a new MessageWriteRequest instance,
encapsulating the response.
TODO : WTF ??? Why do we process the same message twice ???
- In the HeadFilter, we grab the message, which is empty. In fact, this
empty message is used as a marker for the 'end of message'.
Nevertheless, we add this empty message to the WriteRequestQueue
- We ask the processor to flush again the session, but as the session
has already been added into the scheduledForFlush queue, nothing is done
- And we are done with all the process() method. We come back to the
main select loop
- Time to process the session waiting to be flushed now… This is done
with a call to the flush(currentTime) method. We check that we have
sessions ready to be flushed first. If so, we loop on all those sessions.
Note : a session may be marked as ready to be flushed either because we
just have some new message for it, or because a big message hasn't be
written to the client completely n a previous loop.
At first, we remove the session from the queue of scheduled sessions. We
will put it back if the full message hasn't been written later. If the
session is open, we call the pocessor.flushNow() method
- There, we grab he WriteRequestQueue for this session. There is
something obscure done here : we compute some number based on the max
read and write buffer.
TODO : remove all this crapity…
We reset the OP_WRITE flag (it may have been set in a previous call).
The session stores the current buffer being flushed in a special holder.
if it's null, we take the first message from the queue and stores it
into this session.
TODO : This is absolutely useless. At this point, we know exactly where
we left when we wrote it to the client. it's enough to keep the buffer
in the queue, peeking it instead of polling it. We just remove it when
the message has been completely sent.
- We now call the writeBuffer() method, responsible for the writing of
the data to the client.
TODO : There is a totally useless loop in this method, where we try to
write to the client up to 256 times, just in case the client is slow, I
guess. This is overkilling. We have *no idea* why this loop exists,
except that back in the past, some strange bug was fixed with such a
'workaround'
- The buffer is written to the channel, and we get back the number of
written bytes. If we have written all the bytes, we call the
fireMessage() method. The currentWriteRequest is cleared in the session.
The WriteFuture is now set to Written state (useless, as nobody uses
this information).
TODO : Get rid of useless tasks
- In the PotocolCodecFilter, as the WriteRequest is the
encodedWriteRequest, we immediately return.
TODO Why the hell are we calling the messageSent() method at all as we
just don't process the information here ??? probably because we have no
clue about the fact that the WriteRequest is a message at this point. It
has to be fixed, it's sucking useless CPU
- Then we are back, and process the ext message in the queue, which is
the empty message (used as a end-of-message marker)
- The writeBuffer() method just do nothing, as this message is empty. It
just call the messageSent() method to inform the handler that the
message has been sent. So we go up in the chain to the handler, and back.
- There is now a stupid things done (one more …) : as we didn't wrote
anything, the number of written bytes is 0, so we consider that the
socket is full, and we switch the OP_WRITE flag to true. This is a 100%
guarantee that we will do a full select loop again, for nothing…
TODO : FIX ME !!!
- When we are back from the flushNow() method, we get a false, as we
didn't' flushed the empty message. But we are done with the list of
sessions scheduled for a flush.
- and we get back to the select(), which will exit immediately with at
least one selectionKey set with OP_WRITE, as wear still waiting to write
the empty message !!!. That means we will process two reads for one write.
- once we enter again in the mail select loop, the select() returns 1,
we go back to the process() method, do nothing in the read part, but put
back the session onto the scheduledForFlush queue, in order to be
processed again in the flush() call.
- In the flush() method, we reset the OP_WRITE flag, so that we aren't
woke up again, and we are done.
That's it !!! Lot of hacks, lot of useless things, lot of potential
speedup expected !
--
Regards,
Cordialement,
Emmanuel Lécharny
www.nextury.com