APack async header compression

Mikkel Fahnøe Jørgensen <mikkelfj@gmail.com> Fri, 08 December 2017 23:15 UTC

Return-Path: <mikkelfj@gmail.com>
X-Original-To: quic@ietfa.amsl.com
Delivered-To: quic@ietfa.amsl.com
Received: from localhost (localhost [127.0.0.1]) by ietfa.amsl.com (Postfix) with ESMTP id 8FC6E12025C for <quic@ietfa.amsl.com>; Fri, 8 Dec 2017 15:15:08 -0800 (PST)
X-Virus-Scanned: amavisd-new at amsl.com
X-Spam-Flag: NO
X-Spam-Score: -2.698
X-Spam-Level:
X-Spam-Status: No, score=-2.698 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, FREEMAIL_FROM=0.001, HTML_MESSAGE=0.001, RCVD_IN_DNSWL_LOW=-0.7, SPF_PASS=-0.001, UNPARSEABLE_RELAY=0.001] autolearn=ham autolearn_force=no
Authentication-Results: ietfa.amsl.com (amavisd-new); dkim=pass (2048-bit key) header.d=gmail.com
Received: from mail.ietf.org ([4.31.198.44]) by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id F7m5NP73VqXJ for <quic@ietfa.amsl.com>; Fri, 8 Dec 2017 15:15:06 -0800 (PST)
Received: from mail-io0-x22e.google.com (mail-io0-x22e.google.com [IPv6:2607:f8b0:4001:c06::22e]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by ietfa.amsl.com (Postfix) with ESMTPS id CD319120227 for <quic@ietf.org>; Fri, 8 Dec 2017 15:15:05 -0800 (PST)
Received: by mail-io0-x22e.google.com with SMTP id d14so4004620ioc.5 for <quic@ietf.org>; Fri, 08 Dec 2017 15:15:05 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:mime-version:date:message-id:subject:to; bh=mzrvomXquRfBzSdZixYZ6CUHlpYkFVEAv7uy4/VkmUQ=; b=a3b/h5C2klyivcQjvL/WQxr0tu2AMub83YKkN4INTZZiikrrMa7PDgsi1dgZj4a5K1 iqpLGncwMul8f9g2+voC4WxZ7A2cnolVcFvv1O3W7uhjS7nfWjES/xLwI/L3MubB4Cas F7b/VNCGnNlLfE8r77QBwSrc7GUqvvwiijACtVjsVOGnJnRYZy46M+gHiJulyKTn2OYL QHwol5R9TF3ZUi48Dc78/5R4SE9IBhcy1H8IlJq4VLRgtQIiHX+reMnxJlH0sbcu50Dw srI+sgw9wSjSbRlhYK9A2MPMOSFPeL8UOTPQkwgpLScWI6GKxZCnpFrKpwM5M0fU/lxS g1nw==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:mime-version:date:message-id:subject:to; bh=mzrvomXquRfBzSdZixYZ6CUHlpYkFVEAv7uy4/VkmUQ=; b=HqqQipgaiTarvbsu3/mT/1DewFOsUf8qoGZSksFImgUMQF8C0FVmg/TEhSfTZgLrD+ eScW/fI/9uVx7s27AzJZXT1F+ngJEQNvnc4CC3PuGZG84eorZL7CGYHDCmjxfLeT0fbk lNs9roQASbJOyGMoHh9FVttn8bNHDXfqyuonib5NhaX2Q/kzNGKZCKmENN9W7kWFuKmv ZjMot8/pHHmO3EeQA4keCRhvitvSV1hUOkjMyFdfpslN3u8Peu+qZLpcnbgcwf3BjQsU eqFCw3Ui1jXfc1tTfgMUr9CBqHXjuGU0fqOccPYp3ySNGX2f0QIaXR1SvvUjYJYJoRwI Zj8A==
X-Gm-Message-State: AJaThX6oHg4srYXTYsIdd/QFDdY8q0rDNmfhMY1UiwG6HEgZne7eMH6K EzbRnV3Rbeb80iNNB8Oxy0ETz9+Uo/5n409Q9p3cVA==
X-Google-Smtp-Source: AGs4zMZeFn5WyGLoGIv5lHvhCUzeQ+fHDbI/cT471aLP2mNAx+MJFR2DuNfEa/gg5tL3zs7+yHMvpiGGXKsr7POpfxs=
X-Received: by 10.107.3.86 with SMTP id 83mr46688341iod.297.1512774904753; Fri, 08 Dec 2017 15:15:04 -0800 (PST)
Received: from 1058052472880 named unknown by gmailapi.google.com with HTTPREST; Fri, 8 Dec 2017 18:15:01 -0500
From: Mikkel Fahnøe Jørgensen <mikkelfj@gmail.com>
X-Mailer: Airmail (420)
MIME-Version: 1.0
Date: Fri, 08 Dec 2017 18:15:01 -0500
Message-ID: <CAN1APdf2PEeErxCp8H62_BHG7EE07AX_HVj33xfCVtGg8bg8gw@mail.gmail.com>
Subject: APack async header compression
To: IETF QUIC WG <quic@ietf.org>
Content-Type: multipart/alternative; boundary="001a113eb83ad2b205055fdc5a2d"
Archived-At: <https://mailarchive.ietf.org/arch/msg/quic/bYSxg8CwJVBCgesLR_hRVT2MOK4>
X-BeenThere: quic@ietf.org
X-Mailman-Version: 2.1.22
Precedence: list
List-Id: Main mailing list of the IETF QUIC working group <quic.ietf.org>
List-Unsubscribe: <https://www.ietf.org/mailman/options/quic>, <mailto:quic-request@ietf.org?subject=unsubscribe>
List-Archive: <https://mailarchive.ietf.org/arch/browse/quic/>
List-Post: <mailto:quic@ietf.org>
List-Help: <mailto:quic-request@ietf.org?subject=help>
List-Subscribe: <https://www.ietf.org/mailman/listinfo/quic>, <mailto:quic-request@ietf.org?subject=subscribe>
X-List-Received-Date: Fri, 08 Dec 2017 23:15:08 -0000

OK, it turns out this async compression problem is very similar to certain
async problems I have been working with related to distributed garbage
collection, so I wrote up an algorithm based on exchanging bitmaps. At
first it may sound a bit complex and it does require a good bitmap
implementation, but it doesn’t have very much overhead nor any difficult
heuristics. The main cost is a bitmap of dictionary size per active request
aside from the dictionary itself so this is 100 bytes if there are 800
entries in the dynamic table. It also requires a reference count per entry
on the sender side but this count only need to go up to the number of
concurrent requests, so typically one byte. Beware of dragons, I just made
it up, but I do have somewhat similar stuff that is actually working.

The main idea is to have a free bitmap lazily synchronised between sender
and receiver and prevent freeing up entries still in use. There is a level
of HOL but it only delays eviction of old entries. There is only one shared
dictionary.


# APack

An async header compression scheme.

## Problem

We need to send an ordered list name/value pairs a strings from a sender
to a receiver, typically, but not necessarily a HTTP header list. We
call this an exhange, although it is essentially one-directional but we
expect the name/value list to be used for constructing a subsequent
response that we need not be concerned with here.

We want to reduce the size of the exchange without significant
complexity and time/space resource consumption and we must protect the
receiver from abuse.

Also we want to avoid optionally compression on some name/value pairs
because that could compromise secrecy, for example a password field.

In addition we want to have multiple exchanges and allow later exchanges
to benefit from earlier exchanges to increase the compression ratio when
reasonable.

Multiple exchanges can happen overlapped in time. Each exchange happens
over an ordered stream but seperate exhanges happen on separate
uni-directional streams without any implied ordering across streams. We
can have bi-directional streams too, but prefer not when not needed.

The sender and the receiver has shared state across the exhanges and all
exhanges, while active, has access to one or more shared bi-direcitonal
control streams over which global synchronization can be achieved.
However, if we only relied on shared streams we would have head of line
blocking (HOL) unnecessarily introducing dependencies across exhanges.


## Solution

The values are always sent explicitly as a string while the names are
sent via a compression scheme that can improve across exchanges. The
values may be compressed in a simplistic manner, but it carries no
state. Runlength encoding and static huffman encoding is possible, but
we just assume value is sent as is, because it does not affect any other
part of the operation. We call this local string compression.

Hence we focus on names. Names are either sent a strings in the same
form as values. We call this explicit names. Explicit names may be
subject to local string compression. Otherwise names are sent
by reference into a dictionary that is known to both sender and
receiver. We call this implicit names. We have to forms of implicit
names: dynamic and static. Static names are a list of names known in
advance out of band each assigned a unique index. Dynamic names a
references into a dynamic dictionary that is updated by the sender and
read by the receiver. When a dictionary is updated, the name is sent as
an explicit string subject to local string compression.

Our primary concern is now how to update the dicationary such that
subsequent implicit dynamic name references will see the correct
dictionary value.

An exchange is roughly communicated as a list of commands where a
command eithers sends a dynamic dictionary update, or name/value pair
where the name is either explicit, implicit dynamic, or implicit static.
When all name value pairs are sent, a final command indicates
completion.

If we only had one exchange at a time this would be all we needed
because a dictionary could just be updated before using the value.
To conserve space, some dictionary entries must be evicted. This can be
explicit, or implied by a dictionary update because it is known to
overtake an existing location.

Abuse can be avoided by requiring that the sender reuses locations
instead of indefinintely adding new entries and a limit on the number of
entries can be negotiated in advance or out of band.

The above is roughly the HPACK compression scheme without the gritty
practical details.

However, we need to handle multiple exchanges concurrently.

The following will deal with an async solution.


## Asynchronous Problems

We are not concerned with static names or and explicit names because
these are transmitted trivially on single exchange stream.

This leaves us with dynamic dictionary updates and references.

We could send all updates on a shared control stream and all references
on specific exhange stream. The only problem is that the reference might
not have arrived yet, or it might have been replaced or evicted before
the reference arrives.

We could solve this problem by splitting the dictionary into multiple
read only groups, sometimes called an epoch or a checkpoint such that a
reference is known to belong one such. The trick is then to ensure the
group survives long enough via coordination between sender and receiver.
This is not necessarily a bad idea, but since it has been explored, we
take a different approach.


## Asynchronous Solution

When a dictionary entry is updated, this always happens on the control
channel from sender to receiver. When a reference is sent on an exchange
stream the sender increments a reference count locally on the dictionary
entry but does not transmit it. When an exhange completes, this fact is
sent on the control channel along with the exchange id and then
decrements local reference counts on all entries sent through the
exchange. The end of exchange message is called EOX.

When the receiver sees an EOX message on the control channel it first
checks a dead EOX map. If found, the dead entry is removed and no
further action is taken. Otherwise receiver places the EOX message on a
EOX queue that can also be accessed by EOX id. A scan will normally
suffice due to the access pattern.  The EOX queue entry has an empty
eviction bitmap which will be discussed later.

The receiver does not implicitly evict any dictionary entries and the
sender does not evict any entries that do not have refcount zero.
Refcount zero entries are only evicted when space is needed, otherwise
we loose the history needed for efficient compression.

If we assume the dictionary is table with 1000 entries, we can have a
bitmap with 1000 entries. The sender periodically sends eviction bitmaps
that may be runlength compressed.

The receiver processes eviction bitmaps by finding the most recent EOX
entry and merges the new eviction bitmap into it. If the queue is empty
then all entries in the bitmap are evicted immediately. When the
receiver evicts an entry it marks this in a commit bitmap that is
initially empty. The receiver periodically sends the commit bitmap back
to the sender while merging it into a free bitmap and clears the commit
bitmap. The free bitmap is initially completely set but will have been
partially cleared once there are commit bits.

When the sender receives a commit bitmap it merges it into its freemap
that also originally was complete set, but is no more.

When the reciever completes an exchange on the dedicated exchange
channel it looks up the exchange ID in the EOX index. If it finds any it
removes the EOX entry and merges its eviction bitmap with the next older
entry. If there is no older entry the bitmap is merged with the commit
bitmap. If there is no matching EOX entry in the EOX queue, an entry is
made in a dead EOX map.

We can now see how we can allocate new dictionary entries:

The receiver has free bitmap that is initially complete set. A working
copy of the bitmap is saved. This is called the allocation map. It is
not strictly needed, but helpful because it doesn't change when the
commit bitmaps messages are used to update the free bitmap.

The sender maintains a current position in the allocation bitmap which
is originally at the first non-empty bit. (We assume there is one for
now).  The next allocation happens at the current index and the current
index is advanced. The name is send on the control channel along with
the index. We can avoid sending the index with a bit more coodination
but for simplicity lets assume we send the index along.  Eventually we
reach the end of the allocation bitmap. When this happens we xor the
allocation bitmap into the free bitmap to clear the consumed positions
then make a new snapshot of the free bitmap and start over. If we still
do not have a free loation, then we have no free location and we proceed
with sending explicit names on each exhchange separately.

We can avoid sending the index with each name update if we carefully
coordinate when the allocation bitmap is snapshot. But it might not be
worth the complexity.

By snapshotting the free bitmap into the allocation bitmap we ensure
that that we evict older entries first but when we send the index with
each name update we could also choose any arbitrary set bit in the free
bitmap directly and clear that bit.