The identity part usually corresponds to the X request's structure, while the data part is what
follows. In this case the identity part has a length of 12 bytes.
NX maintains in the main memory a cache of the last X messages sent through the wire, divided by
protocol opcode. This cache is named MessageStore. To allow a fast look-up of messages in the
specific MessageStore, NX calculates a MD5 checksum of any new request or reply that has to be
encoded. Any message type has its own method to calculate the MD5 of the identity, while MD5 of
the data part is simply obtained by adding any data byte to the checksum. NX maintains information
about opcode implicitly, by adding messages to the right MessageStore, and saving size in a
specific field of the base Message class.
The MessageStore's method calculating MD5 of identity has to be carefully chosen to not include
those fields that are likely to change across different instances of the same X request. In the
case of PolySegment, the method calculating MD5 of identity is empty, as both Drawable and GContext
are likely to be different in any new request.
When a new PolySegment request is received, NX calculates the checksum of the new message and
searches it in the MessageStore. If the message is found, NX only sends this status information to
the remote peer, together with the position where the message can be retrieved from store and a
differential encoding of all those fields that are not part of the identity checksum.
In case of PolySegment, the fields that need to be differentially encoded are Drawable and GContext.
All atomic values that NX sends over the network have a specific encoding, usually based on a
integer or character cache. Differential encoding is treated in a separate section.
Encoding of status and position in MessageStore cache requires between 2 and 6 bits, depending on
the protocol message. Drawable and GContext (originally 64 bits) can be encoded in as few as 2 to
8 bits. This makes it possible to encode a PolySegment request of 176 bits (22 bytes) in 4 to 14
bits, with a compression ratio of more than 10:1.
If the message cannot be found in cache, proxy encodes the message field by field. It also prepends
the position where the decoding side has to store the message in cache. As it is the encoding side
to mandate the position in cache, we can say that each proxy manages the cache of its remote peer.
In this way, any proxy knows exactly, at any given time, if a message can be retrieved from cache,
without having to use expensive round-trips.
It's worth noting that only the encoding side has to calculate and maintain the messages' checksums,
as the decoding side only needs to save the payload. This greatly reduces the total amount of memory
needed to store the 3000 and more messages that can be contained in each message store.
At first, many can think that X protocol messages present too much “variability” to be effectively
cached. We have found that excluding the variant part from checksum calculation and encoding it
separately, the amount of cache hits can be dramatically increased. Along the years, tuning of the
encoding algorithm has permitted us to reach between 60% to 80% of cache hits on the total amount
of X protocol messages encoded on the wire. For some messages, like graphic requests, images, fonts
and other requests used in common office automation desktop applications, the cache hits can be 100%,
allowing NX to reach compression ratios in the order of 1000:1.
We have observed that message store based encoding not only reduces the amount of bandwidth used by
X protocol, but also obtains very good CPU performances with respect to any other known X compression
method. In fact, given that the caching algorithm is reasonably effective, the time needed to
calculate MD5 of message and send a reference to the remote peer is much lower than time needed to
compress the X protocol by means of a generic algorithm, like ZLIB.