Repository: netty/netty
Issue: HTTP/2 Priority Tree Event Notification

Through investigation and discussion in PR https://github.com/netty/netty/pull/2775 there needs to be more investigation into the current implementation of the `Http2Connection.Listener` interface with respect to tree restructuring and listener notification.


Issue comments:
The HTTP/1.x translation layer needs to be notified when ever a node's parent changes.  The interface for `Http2Connection.Listener.streamPriorityChanged` lends itself to this but is not invoked for all parent change events.  Should this be invoked in a different spot(s)?  For example is the `DefaultHttp2OutboundFlowController$streamPriorityChanged` method correctly updating state with the current invocation of `streamPriorityChanged` in `DefaultHttp2Connection`?

Here is the initial discussion extracted from PR #2775

@nmittler - I would like to make some changes to support tracking the priority information in the HTTP/1.x headers.
**Change Summary**
1. Add method: `Http2Connection.Listener.parentChange(Http2Stream stream, Http2Stream oldParent)` which is invoked in `DefaultHttp2Connection.addChild` (and possibly other spots).
2. Re-order `stream.setPriority(...)` calls to be after the `onXXXRead(...)` calls in `AbstractHttp2ConnectionHandler`

**Potential negatives**
1. The new parentChange method is fine grained and will be invoked once for each listener for each parent change.
2. `DefaultHttp2OutboundFlowController` consumes priority listener events and may be impacted (although I don't think it will be because I don't think I need to change the onXXXWrite flow).
3. (Regardless of implementation) Depending on when the stream events come in it may be past the HTTP/1.x message flow and the event may be ignored.

**Justification**
The HTTP/1.x headers currently list
- Parent Stream Id
- Is Exclusive
- Weight

Weight and IsExclusive can be obtained reliably from the onXXXRead methods.  The parent stream id is currently not being kept up to date in all situations (if tree operations move a node unrelated to the onXXXRead method).  To mitigate this I would like to add a method to the listener interface that is invoked when a parent link changes.
After this change there will still be an ordering issue between when the priority tree events are dispatched (via `stream.setPriority(...)`) and the `onHeadersRead` or `onPriorityRead` event.  The ordering issue is if I subscribe the the priority tree listener interface for events it is possible (in fact likely) that I will not know that the stream exists (because the onXXXRead method hasn't been called).  So to solve this I would need to move the `stream.setPriority(...)` method to be after the onXXXRead method.

@nmittler - Can you explain the conditional statements leading up to setting the stream priority when a header is read on [line 862 on master](https://github.com/netty/netty/blob/master/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandler.java#L862)?  For example what if the first header message contains prioritization information, will we be setting the priority on the stream?  Also what if the stream is in the half-closed local state?

@Scottmitch maybe we can generalize this change a bit.  The current Listener interface has two event handlers:

streamPriorityChanged(Http2Stream stream, Http2Stream previousParent) for simple changes, possibly just changing the parent.

streamPrioritySubtreeChanged(Http2Stream stream, Http2Stream subtreeRoot) when a node was made to be dependent on one of its descendants.  In this case, the subtree starts at the new parent (previously was the descendant).

These events were tailored for use by the outbound flow controller logic, but I think we can simplify this interface so it will work for all use cases.

streamPriorityChanging(Http2Stream stream, Http2Stream previousParent) Called for all affected nodes before they are moved or their subtree is restructured. Need to think about it more, but event order may matter (bottom-up?) to make sure that the outbound flow controller properly removes weight calculations up the tree.

streamPriorityChanged(Http2Stream stream, Http2Stream newParent);
Called after each affect node's priority has changed or it's final position in the tree has been restored. Again, event order may matter (top-down?) ... need to iron out the details.

We likely will want to do this for all affected nodes in order:
1) streamPriorityChanging for all nodes about to be affected
2) make all the changes to the tree/nodes.
3) streamPriorityChanged for all nodes that were affected.

Thoughts?

The generalization you suggest is exactly what I had in mind.  I agree the order of events should be well coordinated to avoid notification at incorrect times when manipulation is still occurring and also to handle the case that you currently are handling when a node is moved into its child tree structure.

@Scottmitch ok sounds great!  Let's get the draft 14 framing PR cherrypicked and I can start working on this.

@nmittler - We can handle the coordination between http2 core codec and the http translation layer in the same manner as the draft-14 updates (I will ultimately submit a PR based upon your cherry picked changes).

@Scottmitch so your PR will be blocked on the priority changes?  Would it be easier for you to just take over this bug and incorporate the changes into your PR?

@nmittler - Do you mean handle this as part of PR https://github.com/netty/netty/pull/2778? If so I think it would be best to handle this as a separate PR to break up concerns. In terms of who does the work on the listener interface (and related) changes and I can take a stab at it if you want.

@Scottmitch that was my thought, since I didn't want to further block #2778.  However, if you can implement it without the priority changes for now, that would definitely make it easier to divide up the work ... and I'll just take care of this in parallel.  Will that work?

@nmittler - That sounds ideal.  Feel free to work on this.  I can apply the updates to the HTTP/1.x translation layer after #2778 is accepted (and likely work with you in parallel again to ensure compatibility/timely completion of my changes).

Here is an example (which can be made more minimal if necessary) of the non-restructure case which demonstrates the issue and may make a good test case.

**Initial State**
Each of the following nodes represents a stream, and the letter is representative of their stream ID.

> ```
>              A
>    /         |          \
>   B          C           D
> /    \    /    \      /     \
> E     F  G      H    I       J
> ```
1. Call `C.setPriority(D, 4, true)` from `DefaultHttp2Connection`

**End Result**

> ```
>              A
>    /                 \
>   B                    D
> /    \                 |
> E     F                C
>                /   /      \   \
>               G   H        I    J
> ```

**Current Implementation Result**
1. `notifyPriorityChanged(C, A)` is called but is the `priorityBytes` in `DefaultHttp2OutboundFlowController` for I and J ever added into C (and up the tree)?
2. There is no notification for I and J even though their parents have changed (new functionality requested by this issue).

@Scottmitch So I think the new logic would do the following:
1. streamPriorityChanging for C, G, H, I, and J
2. Update the tree
3. streamPriorityChanged for G, H, I, J, and C

Does that sound right to you?

@nmittler - I made need some clarification on the meaning of `streamPriorityChang[ed|ing]` but are G, H, I, and J changing priority?  I am fixed on the mindset of notifying when parents change and so I may have tunnel vision.

I think the nodes that are changing priority (if priority is the sum of all immediate decedent priorities) are (bottom up): C, D, A.

The nodes that are changing parents are (top down): C (from A to D), I and J (from D to C)

Getting notified for each element in the affected subtree(s) is useful for the outbound flow controller, which maintains state (i.e. total number of pending bytes for the subtree) at each node.  I was imagining that streamPriorityChanging would allow the flow controller to remove the pending bytes upward in the tree for that node.  When streamPriorityChanged is invoked, it would then add the pending bytes up the tree for that node ... if it was assigned a new parent, that's fine.

So all of these events are useful for this case, but will they cause confusion for other cases?  We can always throw a ton of documentation at the interface :)

I think I see where you are coming from by referencing `incrementPriorityBytes`.  I think my mental model of the interface was a bit different:

**Notify in a top down manner**
_parentChanging(Http2Stream stream, Http2Stream newParent)_
tree changes happen here
parentChanged(Http2Stream stream, Http2Stream oldParent)

This structures the interface more around general tree operations and I think decreases the chances of clients missing relevant events (or double counting some events).  I would need to think about how this generalizes to the case when we are moving a node into its decedent sub-tree.

I put the changing events in _italics_ because I'm not sure if they are needed?

In the previous example the order of events -> and resulting flow controller operations:
parentChanged(C, A) -> subtract C.priority from A up to root, add C.priority to C.parent up to root
parentChanged(I, D) -> subtract I.priority from D up to root, add I.priority to I.parent up to root
parentChanged(J, D) -> subtract J.priority from D up to root, add J.priority to J.parent up to root

Lets try an example where a node is moved into its own sub-tree.  The spec conveniently has an example for this case http://http2.github.io/http2-spec/index.html#rfc.section.5.3.3.

**Non-exclusive order of events**
parentChanged(D, C) -> subtract D.priority from C up to root, add D.priority to D.parent up to root
parentChanged(A, ?) -> subtract A.priority from ? up to root, add A.priority to A.parent up to root

**Exclusive order of events**
parentChanged(D, C) -> subtract D.priority from C up to root, add D.priority to D.parent up to root
parentChanged(A, ?) -> subtract A.priority from ? up to root, add A.priority to A.parent up to root
parentChanged(F, D) -> subtract F.priority from D up to root, add F.priority to F.parent up to root

I think these both work, but does it generalize?

This interface also facilitates the information I am after, being able to generate messages when individual node parent change events occur.  If this generalizes then it also reduces the required interface for priority tree events from 2 to 1.

@nmittler - I opened PR https://github.com/netty/netty/pull/2802 to address this issue.

@Scottmitch can we close this now?

@nmittler - Closed.

@nmittler & @normanmaurer reviewed PR #2802 and @nmittler cherry picked into master.

Closing this bug/improvement.


Pull request review comments:
@nmittler - I could use some feedback/advice from you here.

I think we might need to notify the listener in 2 passes: once before we make the changes and once after the changes are complete.

Notifying the listener before the changes allows the flow controller to properly decrement pendingBytes for a node from it's branch (up the tree) ... we need the previous tree structure in place to do this.

Agreed, we should definitely have a unit test for this if we don't already :). 

If we have the pre/post-change events as I suggested above, this method will be replaced by the new event handlers.  On pre-change, we decrement the bytes up the old branch.  On post-change we add them to the new branch.  

@nmittler - Verify this logic please.

@nmittler - Verify this logic please.

@Scottmitch this looks right.

@Scottmitch this looks right

Not sure out of my head if listeners is an ArrayList if so you should better use `for (int i = 0; i < 0; i++) { ... }` to reduce object creation as the foreachloop will create a new Iterator everytime.

Coouldn't we use let `DefaultStream.parent()` use `DefaultStream` as return type and so remove the cast ?

Same as above

Listeners is a set (hashset), but the new functionality I added uses an ArrayList with a foreach.  I will update to a traditional for loop.

Done.

This code has changed and the cast is no longer necessary, but all other cases in this class have been updated.

@nmittler - Note that I changed the new interface from 'childRemoving' to 'parentChanging'.  I feel it makes the interface more consistent to be notifying in terms of parent events.  I updated the outbound flow controller & unit tests accordingly.

parentChanging sounds good to me :)

@Scottmitch Agreed, parentChanging sounds good.

@nmittler - Can you confirm this is the correct state after removal?  For example, is there another way I should be removing A to test if A's priority bytes is just A (because it is its own tree)?

If you're opening this up for testing, maybe make all mutation methods private?

just to simplify things, you probably only need to set the priority on one of them.  You should also verify that no structure changing events were received.

nit: maybe make the verification a method ... might make this a little more readable.

maybe move this to below public methods?

nit: maybe make a state(stream) method that does the cast?

or maybe even just priorityBytes(stream) ... that might be cleaner since that's all you're looking for.

Update javadoc ... You're "removing" a node, not "adding" one.

I'm actually not sure the content of the removed state matters much since it won't be in the tree anymore anyway.  Can we verify that the connection's priorityBytes is correct?

Done.

Done.

Done.

Done.

Done.

Good catch


Patch that resolved the issue:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java
index 8b4ec6f1feb..58dc6fe4285 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java
@@ -15,22 +15,34 @@
 
 package io.netty.handler.codec.http2;
 
+import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
+import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
+import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
+import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
+import static io.netty.handler.codec.http2.Http2CodecUtil.immediateRemovalPolicy;
+import static io.netty.handler.codec.http2.Http2Exception.format;
+import static io.netty.handler.codec.http2.Http2Exception.protocolError;
+import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
+import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
+import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
+import static io.netty.handler.codec.http2.Http2Stream.State.IDLE;
+import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
+import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_LOCAL;
+import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE;
 import io.netty.handler.codec.http2.Http2StreamRemovalPolicy.Action;
 import io.netty.util.collection.IntObjectHashMap;
 import io.netty.util.collection.IntObjectMap;
 import io.netty.util.collection.PrimitiveCollections;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
 
-import static io.netty.handler.codec.http2.Http2CodecUtil.*;
-import static io.netty.handler.codec.http2.Http2Exception.*;
-import static io.netty.handler.codec.http2.Http2Stream.State.*;
-
 /**
  * Simple implementation of {@link Http2Connection}.
  */
@@ -47,7 +59,8 @@ public class DefaultHttp2Connection implements Http2Connection {
     /**
      * Creates a connection with an immediate stream removal policy.
      *
-     * @param server whether or not this end-point is the server-side of the HTTP/2 connection.
+     * @param server
+     *            whether or not this end-point is the server-side of the HTTP/2 connection.
      */
     public DefaultHttp2Connection(boolean server) {
         this(server, immediateRemovalPolicy());
@@ -56,11 +69,12 @@ public DefaultHttp2Connection(boolean server) {
     /**
      * Creates a new connection with the given settings.
      *
-     * @param server whether or not this end-point is the server-side of the HTTP/2 connection.
-     * @param removalPolicy the policy to be used for removal of closed stream.
+     * @param server
+     *            whether or not this end-point is the server-side of the HTTP/2 connection.
+     * @param removalPolicy
+     *            the policy to be used for removal of closed stream.
      */
-    public DefaultHttp2Connection(boolean server,
-            Http2StreamRemovalPolicy removalPolicy) {
+    public DefaultHttp2Connection(boolean server, Http2StreamRemovalPolicy removalPolicy) {
         if (removalPolicy == null) {
             throw new NullPointerException("removalPolicy");
         }
@@ -147,7 +161,7 @@ private void removeStream(DefaultStream stream) {
 
         // Remove it from the map and priority tree.
         streamMap.remove(stream.id());
-        ((DefaultStream) stream.parent()).removeChild(stream);
+        stream.parent().removeChild(stream);
     }
 
     private void activate(DefaultStream stream) {
@@ -264,7 +278,7 @@ public final int totalChildWeights() {
         }
 
         @Override
-        public final Http2Stream parent() {
+        public final DefaultStream parent() {
             return parent;
         }
 
@@ -307,12 +321,10 @@ public final Http2Stream child(int streamId) {
         }
 
         @Override
-        public Http2Stream setPriority(int parentStreamId, short weight, boolean exclusive)
-                throws Http2Exception {
+        public Http2Stream setPriority(int parentStreamId, short weight, boolean exclusive) throws Http2Exception {
             if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) {
                 throw new IllegalArgumentException(String.format(
-                        "Invalid weight: %d.  Must be between %d and %d (inclusive).", weight,
-                        MIN_WEIGHT, MAX_WEIGHT));
+                        "Invalid weight: %d.  Must be between %d and %d (inclusive).", weight, MIN_WEIGHT, MAX_WEIGHT));
             }
 
             // Get the parent stream.
@@ -324,52 +336,23 @@ public Http2Stream setPriority(int parentStreamId, short weight, boolean exclusi
             // Already have a priority. Re-prioritize the stream.
             weight(weight);
 
-            boolean needToRestructure = newParent.isDescendantOf(this);
-            DefaultStream oldParent = (DefaultStream) parent();
-            try {
-                if (newParent == oldParent && !exclusive) {
-                    // No changes were made to the tree structure.
-                    return this;
-                }
-
-                // Break off the priority branch from it's current parent.
-                oldParent.removeChildBranch(this);
-
-                if (needToRestructure) {
-                    // Adding a circular dependency (priority<->newParent). Break off the new
-                    // parent's branch and add it above this priority.
-                    ((DefaultStream) newParent.parent()).removeChildBranch(newParent);
-                    oldParent.addChild(newParent, false);
-                }
-
-                // Add the priority under the new parent.
-                newParent.addChild(this, exclusive);
-                return this;
-            } finally {
-                // Notify observers.
-                if (needToRestructure) {
-                    notifyPrioritySubtreeChanged(this, newParent);
+            if (newParent != parent() || exclusive) {
+                List<ParentChangedEvent> events = null;
+                if (newParent.isDescendantOf(this)) {
+                    events = new ArrayList<ParentChangedEvent>(2 + (exclusive ? newParent.children().size() : 0));
+                    parent.takeChild(newParent, false, events);
                 } else {
-                    notifyPriorityChanged(this, oldParent);
+                    events = new ArrayList<ParentChangedEvent>(1 + (exclusive ? newParent.children().size() : 0));
                 }
+                newParent.takeChild(this, exclusive, events);
+                notifyParentChanged(events);
             }
-        }
-
-        private void notifyPriorityChanged(Http2Stream stream, Http2Stream previousParent) {
-            for (Listener listener : listeners) {
-                listener.streamPriorityChanged(stream, previousParent);
-            }
-        }
 
-        private void notifyPrioritySubtreeChanged(Http2Stream stream, Http2Stream subtreeRoot) {
-            for (Listener listener : listeners) {
-                listener.streamPrioritySubtreeChanged(stream, subtreeRoot);
-            }
+            return this;
         }
 
         @Override
-        public Http2Stream verifyState(Http2Error error, State... allowedStates)
-                throws Http2Exception {
+        public Http2Stream verifyState(Http2Error error, State... allowedStates) throws Http2Exception {
             for (State allowedState : allowedStates) {
                 if (state == allowedState) {
                     return this;
@@ -381,14 +364,14 @@ public Http2Stream verifyState(Http2Error error, State... allowedStates)
         @Override
         public Http2Stream openForPush() throws Http2Exception {
             switch (state) {
-                case RESERVED_LOCAL:
-                    state = HALF_CLOSED_REMOTE;
-                    break;
-                case RESERVED_REMOTE:
-                    state = HALF_CLOSED_LOCAL;
-                    break;
-                default:
-                    throw protocolError("Attempting to open non-reserved stream for push");
+            case RESERVED_LOCAL:
+                state = HALF_CLOSED_REMOTE;
+                break;
+            case RESERVED_REMOTE:
+                state = HALF_CLOSED_LOCAL;
+                break;
+            default:
+                throw protocolError("Attempting to open non-reserved stream for push");
             }
             activate(this);
             return this;
@@ -423,15 +406,15 @@ private void deactivate(DefaultStream stream) {
         @Override
         public Http2Stream closeLocalSide() {
             switch (state) {
-                case OPEN:
-                    state = HALF_CLOSED_LOCAL;
-                    notifyHalfClosed(this);
-                    break;
-                case HALF_CLOSED_LOCAL:
-                    break;
-                default:
-                    close();
-                    break;
+            case OPEN:
+                state = HALF_CLOSED_LOCAL;
+                notifyHalfClosed(this);
+                break;
+            case HALF_CLOSED_LOCAL:
+                break;
+            default:
+                close();
+                break;
             }
             return this;
         }
@@ -439,15 +422,15 @@ public Http2Stream closeLocalSide() {
         @Override
         public Http2Stream closeRemoteSide() {
             switch (state) {
-                case OPEN:
-                    state = HALF_CLOSED_REMOTE;
-                    notifyHalfClosed(this);
-                    break;
-                case HALF_CLOSED_REMOTE:
-                    break;
-                default:
-                    close();
-                    break;
+            case OPEN:
+                state = HALF_CLOSED_REMOTE;
+                notifyHalfClosed(this);
+                break;
+            case HALF_CLOSED_REMOTE:
+                break;
+            default:
+                close();
+                break;
             }
             return this;
         }
@@ -469,15 +452,21 @@ public final boolean localSideOpen() {
         }
 
         final DefaultEndpoint createdBy() {
-            return localEndpoint.createdStreamId(id)? localEndpoint : remoteEndpoint;
+            return localEndpoint.createdStreamId(id) ? localEndpoint : remoteEndpoint;
         }
 
         final void weight(short weight) {
-            if (parent != null && weight != this.weight) {
-                int delta = weight - this.weight;
-                parent.totalChildWeights += delta;
+            if (weight != this.weight) {
+                if (parent != null) {
+                    int delta = weight - this.weight;
+                    parent.totalChildWeights += delta;
+                }
+                final short oldWeight = this.weight;
+                this.weight = weight;
+                for (Listener l : listeners) {
+                    l.onWeightChanged(this, oldWeight);
+                }
             }
-            this.weight = weight;
         }
 
         final IntObjectMap<DefaultStream> removeAllChildren() {
@@ -492,54 +481,101 @@ final IntObjectMap<DefaultStream> removeAllChildren() {
         }
 
         /**
-         * Adds a child to this priority. If exclusive is set, any children of this node are moved
-         * to being dependent on the child.
+         * Adds a child to this priority. If exclusive is set, any children of this node are moved to being dependent on
+         * the child.
          */
-        final void addChild(DefaultStream child, boolean exclusive) {
+        final void takeChild(DefaultStream child, boolean exclusive, List<ParentChangedEvent> events) {
+            DefaultStream oldParent = child.parent();
+            events.add(new ParentChangedEvent(child, oldParent));
+            notifyParentChanging(child, this);
+            child.parent = this;
+
             if (exclusive) {
                 // If it was requested that this child be the exclusive dependency of this node,
                 // move any previous children to the child node, becoming grand children
                 // of this node.
                 for (DefaultStream grandchild : removeAllChildren().values(DefaultStream.class)) {
-                    child.addChild(grandchild, false);
+                    child.takeChild(grandchild, false, events);
                 }
             }
 
-            child.parent = this;
             if (children.put(child.id(), child) == null) {
                 totalChildWeights += child.weight();
             }
+
+            if (oldParent != null && oldParent.children.remove(child.id()) != null) {
+                oldParent.totalChildWeights -= child.weight();
+            }
         }
 
         /**
-         * Removes the child priority and moves any of its dependencies to being direct dependencies
-         * on this node.
+         * Removes the child priority and moves any of its dependencies to being direct dependencies on this node.
          */
         final void removeChild(DefaultStream child) {
             if (children.remove(child.id()) != null) {
+                List<ParentChangedEvent> events = new ArrayList<ParentChangedEvent>(1 + child.children.size());
+                events.add(new ParentChangedEvent(child, child.parent()));
+                notifyParentChanging(child, null);
                 child.parent = null;
                 totalChildWeights -= child.weight();
 
                 // Move up any grand children to be directly dependent on this node.
                 for (DefaultStream grandchild : child.children.values(DefaultStream.class)) {
-                    addChild(grandchild, false);
+                    takeChild(grandchild, false, events);
                 }
+
+                notifyParentChanged(events);
             }
         }
+    }
+
+    private static IntObjectMap<DefaultStream> newChildMap() {
+        return new IntObjectHashMap<DefaultStream>(4);
+    }
+
+    /**
+     * Allows a correlation to be made between a stream and its old parent before a parent change occurs
+     */
+    private final class ParentChangedEvent {
+        private Http2Stream stream;
+        private Http2Stream oldParent;
 
         /**
-         * Removes the child priority but unlike {@link #removeChild}, leaves its branch unaffected.
+         * Create a new instance
+         * @param stream The stream who has had a parent change
+         * @param oldParent The previous parent
          */
-        final void removeChildBranch(DefaultStream child) {
-            if (children.remove(child.id()) != null) {
-                child.parent = null;
-                totalChildWeights -= child.weight();
+        public ParentChangedEvent(Http2Stream stream, Http2Stream oldParent) {
+            this.stream = stream;
+            this.oldParent = oldParent;
+        }
+
+        /**
+         * Notify all listeners of the tree change event
+         * @param l The listener to notify
+         */
+        public void notifyListener(Listener l) {
+            l.priorityTreeParentChanged(stream, oldParent);
+        }
+    }
+
+    /**
+     * Notify all listeners of the priority tree change events (in ascending order)
+     * @param events The events (top down order) which have changed
+     */
+    private void notifyParentChanged(List<ParentChangedEvent> events) {
+        for (int i = 0; i < events.size(); ++i) {
+            ParentChangedEvent event = events.get(i);
+            for (Listener l : listeners) {
+                event.notifyListener(l);
             }
         }
     }
 
-    private static IntObjectMap<DefaultStream> newChildMap() {
-        return new IntObjectHashMap<DefaultStream>(4);
+    private void notifyParentChanging(Http2Stream stream, Http2Stream newParent) {
+        for (Listener l : listeners) {
+            l.priorityTreeParentChanging(stream, newParent);
+        }
     }
 
     /**
@@ -619,7 +655,7 @@ private final class DefaultEndpoint implements Endpoint {
         public int nextStreamId() {
             // For manually created client-side streams, 1 is reserved for HTTP upgrade, so
             // start at 3.
-            return nextStreamId > 1? nextStreamId : nextStreamId + 2;
+            return nextStreamId > 1 ? nextStreamId : nextStreamId + 2;
         }
 
         @Override
@@ -661,8 +697,7 @@ public boolean isServer() {
         }
 
         @Override
-        public DefaultStream reservePushStream(int streamId, Http2Stream parent)
-                throws Http2Exception {
+        public DefaultStream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception {
             if (parent == null) {
                 throw protocolError("Parent stream missing");
             }
@@ -689,12 +724,15 @@ public DefaultStream reservePushStream(int streamId, Http2Stream parent)
         private void addStream(DefaultStream stream) {
             // Add the stream to the map and priority tree.
             streamMap.put(stream.id(), stream);
-            connectionStream.addChild(stream, false);
+            List<ParentChangedEvent> events = new ArrayList<ParentChangedEvent>(1);
+            connectionStream.takeChild(stream, false, events);
 
             // Notify the observers of the event.
             for (Listener listener : listeners) {
                 listener.streamAdded(stream);
             }
+
+            notifyParentChanged(events);
         }
 
         @Override
@@ -732,7 +770,7 @@ public int lastStreamCreated() {
 
         @Override
         public int lastKnownStream() {
-            return isGoAwayReceived()? lastKnownStream : lastStreamCreated;
+            return isGoAwayReceived() ? lastKnownStream : lastStreamCreated;
         }
 
         @Override
@@ -775,12 +813,11 @@ private void verifyStreamId(int streamId) throws Http2Exception {
                 throw protocolError("No more streams can be created on this connection");
             }
             if (streamId < nextStreamId) {
-                throw protocolError("Request stream %d is behind the next expected stream %d",
-                        streamId, nextStreamId);
+                throw protocolError("Request stream %d is behind the next expected stream %d", streamId, nextStreamId);
             }
             if (!createdStreamId(streamId)) {
-                throw protocolError("Request stream %d is not correct for %s connection", streamId,
-                        server ? "server" : "client");
+                throw protocolError("Request stream %d is not correct for %s connection", streamId, server ? "server"
+                        : "client");
             }
         }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java
index 7fe9527f42d..815b7cafab4 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java
@@ -39,8 +39,8 @@
 public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowController {
 
     /**
-     * A comparators that sorts priority nodes in ascending order by the amount of priority data
-     * available for its subtree.
+     * A comparators that sorts priority nodes in ascending order by the amount of priority data available for its
+     * subtree.
      */
     private static final Comparator<Http2Stream> DATA_WEIGHT = new Comparator<Http2Stream>() {
         private static final int MAX_DATA_THRESHOLD = Integer.MAX_VALUE / 256;
@@ -76,8 +76,7 @@ public DefaultHttp2OutboundFlowController(Http2Connection connection, Http2Frame
         this.frameWriter = frameWriter;
 
         // Add a flow state for the connection.
-        connection.connectionStream().outboundFlow(
-                new OutboundFlowState(connection.connectionStream()));
+        connection.connectionStream().outboundFlow(new OutboundFlowState(connection.connectionStream()));
 
         // Register for notification of new streams.
         connection.addListener(new Http2ConnectionAdapter() {
@@ -104,19 +103,19 @@ public void streamInactive(Http2Stream stream) {
             }
 
             @Override
-            public void streamPriorityChanged(Http2Stream stream, Http2Stream previousParent) {
-                if (stream.parent() != previousParent) {
-                    // The parent changed, move the priority bytes to the new parent.
-                    int priorityBytes = state(stream).priorityBytes();
-                    state(previousParent).incrementPriorityBytes(-priorityBytes);
-                    state(stream.parent()).incrementPriorityBytes(priorityBytes);
+            public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
+                Http2Stream parent = stream.parent();
+                if (parent != null) {
+                    state(parent).incrementPriorityBytes(state(stream).priorityBytes());
                 }
             }
 
             @Override
-            public void streamPrioritySubtreeChanged(Http2Stream stream, Http2Stream subtreeRoot) {
-                // Reset the priority bytes for the entire subtree.
-                resetSubtree(subtreeRoot);
+            public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
+                Http2Stream parent = stream.parent();
+                if (parent != null) {
+                    state(parent).incrementPriorityBytes(-state(stream).priorityBytes());
+                }
             }
         });
     }
@@ -216,8 +215,8 @@ private OutboundFlowState state(int streamId) {
     }
 
     /**
-     * Attempts to get the {@link OutboundFlowState} for the given stream. If not available, raises
-     * a {@code PROTOCOL_ERROR}.
+     * Attempts to get the {@link OutboundFlowState} for the given stream. If not available, raises a
+     * {@code PROTOCOL_ERROR}.
      */
     private OutboundFlowState stateOrFail(int streamId) throws Http2Exception {
         OutboundFlowState state = state(streamId);
@@ -243,26 +242,6 @@ private void flush() {
         }
     }
 
-    /**
-     * Resets the priority bytes for the given subtree following a restructuring of the priority
-     * tree.
-     */
-    private void resetSubtree(Http2Stream subtree) {
-        // Reset the state priority bytes for this node to its pending bytes and propagate the
-        // delta required for this change up the tree. It's important to note that the total number
-        // of priority bytes for this subtree hasn't changed. As we traverse the subtree we will
-        // subtract off values from the parent of this tree, but we'll add them back later as we
-        // traverse the rest of the subtree.
-        OutboundFlowState state = state(subtree);
-        int delta = state.pendingBytes - state.priorityBytes;
-        state.incrementPriorityBytes(delta);
-
-        // Now recurse this operation for each child.
-        for (Http2Stream child : subtree.children()) {
-            resetSubtree(child);
-        }
-    }
-
     /**
      * Writes as many pending bytes as possible, according to stream priority.
      */
@@ -279,10 +258,11 @@ private void writePendingBytes() throws Http2Exception {
     }
 
     /**
-     * Recursively traverses the priority tree rooted at the given node. Attempts to write the
-     * allowed bytes for the streams in this sub tree based on their weighted priorities.
+     * Recursively traverses the priority tree rooted at the given node. Attempts to write the allowed bytes for the
+     * streams in this sub tree based on their weighted priorities.
      *
-     * @param allowance an allowed number of bytes that may be written to the streams in this subtree
+     * @param allowance
+     *            an allowed number of bytes that may be written to the streams in this subtree
      */
     private void writeAllowedBytes(Http2Stream stream, int allowance) throws Http2Exception {
         // Write the allowed bytes for this node. If not all of the allowance was used,
@@ -353,8 +333,7 @@ private void writeAllowedBytes(Http2Stream stream, int allowance) throws Http2Ex
             int weight = next.weight();
 
             // Determine the value (in bytes) of a single unit of weight.
-            double dataToWeightRatio =
-                    min(unallocatedBytes, remainingWindow) / (double) remainingWeight;
+            double dataToWeightRatio = min(unallocatedBytes, remainingWindow) / (double) remainingWeight;
             unallocatedBytes -= nextState.unallocatedPriorityBytes();
             remainingWeight -= weight;
 
@@ -397,7 +376,7 @@ private void writeAllowedBytes(Http2Stream stream, int allowance) throws Http2Ex
     /**
      * The outbound flow control state for a single stream.
      */
-    private final class OutboundFlowState implements FlowState {
+    final class OutboundFlowState implements FlowState {
         private final Queue<Frame> pendingWriteQueue;
         private final Http2Stream stream;
         private int window = initialWindowSize;
@@ -405,7 +384,7 @@ private final class OutboundFlowState implements FlowState {
         private int priorityBytes;
         private int allocatedPriorityBytes;
 
-        OutboundFlowState(Http2Stream stream) {
+        private OutboundFlowState(Http2Stream stream) {
             this.stream = stream;
             pendingWriteQueue = new ArrayDeque<Frame>(2);
         }
@@ -416,13 +395,12 @@ public int window() {
         }
 
         /**
-         * Increments the flow control window for this stream by the given delta and returns the new
-         * value.
+         * Increments the flow control window for this stream by the given delta and returns the new value.
          */
-        int incrementStreamWindow(int delta) throws Http2Exception {
+        private int incrementStreamWindow(int delta) throws Http2Exception {
             if (delta > 0 && Integer.MAX_VALUE - delta < window) {
-                throw new Http2StreamException(stream.id(), FLOW_CONTROL_ERROR,
-                        "Window size overflow for stream: " + stream.id());
+                throw new Http2StreamException(stream.id(), FLOW_CONTROL_ERROR, "Window size overflow for stream: "
+                        + stream.id());
             }
             int previouslyStreamable = streamableBytes();
             window += delta;
@@ -441,11 +419,10 @@ int writableWindow() {
         }
 
         /**
-         * Returns the number of pending bytes for this node that will fit within the
-         * {@link #window}. This is used for the priority algorithm to determine the aggregate total
-         * for {@link #priorityBytes} at each node. Each node only takes into account it's stream
-         * window so that when a change occurs to the connection window, these values need not
-         * change (i.e. no tree traversal is required).
+         * Returns the number of pending bytes for this node that will fit within the {@link #window}. This is used for
+         * the priority algorithm to determine the aggregate total for {@link #priorityBytes} at each node. Each node
+         * only takes into account it's stream window so that when a change occurs to the connection window, these
+         * values need not change (i.e. no tree traversal is required).
          */
         int streamableBytes() {
             return max(0, min(pendingBytes, window));
@@ -461,30 +438,28 @@ int priorityBytes() {
         /**
          * Used by the priority algorithm to allocate bytes to this stream.
          */
-        void allocatePriorityBytes(int bytes) {
+        private void allocatePriorityBytes(int bytes) {
             allocatedPriorityBytes += bytes;
         }
 
         /**
-         * Used by the priority algorithm to get the intermediate allocation of bytes to this
-         * stream.
+         * Used by the priority algorithm to get the intermediate allocation of bytes to this stream.
          */
         int allocatedPriorityBytes() {
             return allocatedPriorityBytes;
         }
 
         /**
-         * Used by the priority algorithm to determine the number of writable bytes that have not
-         * yet been allocated.
+         * Used by the priority algorithm to determine the number of writable bytes that have not yet been allocated.
          */
-        int unallocatedPriorityBytes() {
+        private int unallocatedPriorityBytes() {
             return priorityBytes - allocatedPriorityBytes;
         }
 
         /**
          * Creates a new frame with the given values but does not add it to the pending queue.
          */
-        Frame newFrame(ChannelHandlerContext ctx, ChannelPromise promise, ByteBuf data,
+        private Frame newFrame(ChannelHandlerContext ctx, ChannelPromise promise, ByteBuf data,
                 int padding, boolean endStream) {
             return new Frame(ctx, new ChannelPromiseAggregator(promise), data, padding, endStream);
         }
@@ -497,8 +472,7 @@ boolean hasFrame() {
         }
 
         /**
-         * Returns the the head of the pending queue, or {@code null} if empty or the current window
-         * size is zero.
+         * Returns the the head of the pending queue, or {@code null} if empty or the current window size is zero.
          */
         Frame peek() {
             if (window > 0) {
@@ -510,23 +484,22 @@ Frame peek() {
         /**
          * Clears the pending queue and writes errors for each remaining frame.
          */
-        void clear() {
+        private void clear() {
             for (;;) {
                 Frame frame = pendingWriteQueue.poll();
                 if (frame == null) {
                     break;
                 }
-                frame.writeError(format(STREAM_CLOSED,
-                        "Stream closed before write could take place"));
+                frame.writeError(format(STREAM_CLOSED, "Stream closed before write could take place"));
             }
         }
 
         /**
-         * Writes up to the number of bytes from the pending queue. May write less if limited by the
-         * writable window, by the number of pending writes available, or because a frame does not
-         * support splitting on arbitrary boundaries.
+         * Writes up to the number of bytes from the pending queue. May write less if limited by the writable window, by
+         * the number of pending writes available, or because a frame does not support splitting on arbitrary
+         * boundaries.
          */
-        int writeBytes(int bytes) throws Http2Exception {
+        private int writeBytes(int bytes) throws Http2Exception {
             int bytesWritten = 0;
             if (!stream.localSideOpen()) {
                 return bytesWritten;
@@ -553,8 +526,7 @@ int writeBytes(int bytes) throws Http2Exception {
         }
 
         /**
-         * Recursively increments the priority bytes for this branch in the priority tree starting
-         * at the current node.
+         * Recursively increments the priority bytes for this branch in the priority tree starting at the current node.
          */
         private void incrementPriorityBytes(int numBytes) {
             if (numBytes != 0) {
@@ -604,9 +576,9 @@ void enqueue() {
             }
 
             /**
-             * Increments the number of pending bytes for this node. If there was any change to the
-             * number of bytes that fit into the stream window, then {@link #incrementPriorityBytes} to
-             * recursively update this branch of the priority tree.
+             * Increments the number of pending bytes for this node. If there was any change to the number of bytes that
+             * fit into the stream window, then {@link #incrementPriorityBytes} to recursively update this branch of the
+             * priority tree.
              */
             private void incrementPendingBytes(int numBytes) {
                 int previouslyStreamable = streamableBytes();
@@ -646,8 +618,8 @@ void write() throws Http2Exception {
             }
 
             /**
-             * Discards this frame, writing an error. If this frame is in the pending queue, the
-             * unwritten bytes are removed from this branch of the priority tree.
+             * Discards this frame, writing an error. If this frame is in the pending queue, the unwritten bytes are
+             * removed from this branch of the priority tree.
              */
             void writeError(Http2Exception cause) {
                 decrementPendingBytes(data.readableBytes());
@@ -656,12 +628,13 @@ void writeError(Http2Exception cause) {
             }
 
             /**
-             * Creates a new frame that is a view of this frame's data buffer starting at the
-             * current read index with the given number of bytes. The reader index on the input
-             * frame is then advanced by the number of bytes. The returned frame will not have
-             * end-of-stream set and it will not be automatically placed in the pending queue.
+             * Creates a new frame that is a view of this frame's data buffer starting at the current read index with
+             * the given number of bytes. The reader index on the input frame is then advanced by the number of bytes.
+             * The returned frame will not have end-of-stream set and it will not be automatically placed in the pending
+             * queue.
              *
-             * @param maxBytes the maximum number of bytes that is allowed in the created frame.
+             * @param maxBytes
+             *            the maximum number of bytes that is allowed in the created frame.
              * @return the partial frame.
              */
             Frame split(int maxBytes) {
@@ -673,8 +646,7 @@ Frame split(int maxBytes) {
             }
 
             /**
-             * If this frame is in the pending queue, decrements the number of pending bytes for the
-             * stream.
+             * If this frame is in the pending queue, decrements the number of pending bytes for the stream.
              */
             void decrementPendingBytes(int bytes) {
                 if (enqueued) {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Connection.java
index 48c2167c27e..f8d9933164e 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Connection.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Connection.java
@@ -57,33 +57,30 @@ interface Listener {
         void streamRemoved(Http2Stream stream);
 
         /**
-         * Notifies the listener that the priority for the stream has changed. The parent of the
-         * stream may have changed, so the previous parent is also provided.
-         * <p>
-         * Either this method or {@link #streamPrioritySubtreeChanged} will be called, but not both
-         * for a single change. This method is called for simple priority changes. If a priority
-         * change causes a circular dependency between the stream and one of its descendants, the
-         * subtree must be restructured causing {@link #streamPrioritySubtreeChanged} instead.
-         *
-         * @param stream the stream for which the priority has changed.
-         * @param previousParent the previous parent of the stream. May be the same as its current
-         *            parent if unchanged.
+         * Notifies the listener that a priority tree parent change has occurred. This method will be invoked
+         * in a top down order relative to the priority tree. This method will also be invoked after all tree
+         * structure changes have been made and the tree is in steady state relative to the priority change
+         * which caused the tree structure to change.
+         * @param stream The stream which had a parent change (new parent and children will be steady state)
+         * @param oldParent The old parent which {@code stream} used to be a child of (may be {@code null})
          */
-        void streamPriorityChanged(Http2Stream stream, Http2Stream previousParent);
+        void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent);
 
         /**
-         * Called when a priority change for a stream creates a circular dependency between the
-         * stream and one of its descendants. This requires a restructuring of the priority tree.
-         * <p>
-         * Either this method or {@link #streamPriorityChanged} will be called, but not both for a
-         * single change. For simple changes that do not cause the tree to be restructured,
-         * {@link #streamPriorityChanged} will be called instead.
-         *
-         * @param stream the stream for which the priority has changed, causing the tree to be
-         *            restructured.
-         * @param subtreeRoot the new root of the subtree that has changed.
+         * Notifies the listener that a parent dependency is about to change
+         * This is called while the tree is being restructured and so the tree
+         * structure is not necessarily steady state.
+         * @param stream The stream which the parent is about to change to {@code newParent}
+         * @param newParent The stream which will be the parent of {@code stream}
+         */
+        void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent);
+
+        /**
+         * Notifies the listener that the weight has changed for {@code stream}
+         * @param stream The stream which the weight has changed
+         * @param oldWeight The old weight for {@code stream}
          */
-        void streamPrioritySubtreeChanged(Http2Stream stream, Http2Stream subtreeRoot);
+        void onWeightChanged(Http2Stream stream, short oldWeight);
 
         /**
          * Called when a GO_AWAY frame has either been sent or received for the connection.
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionAdapter.java
index d349d2847da..5ac64ee2e09 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionAdapter.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionAdapter.java
@@ -40,14 +40,18 @@ public void streamRemoved(Http2Stream stream) {
     }
 
     @Override
-    public void streamPriorityChanged(Http2Stream stream, Http2Stream previousParent) {
+    public void goingAway() {
     }
 
     @Override
-    public void streamPrioritySubtreeChanged(Http2Stream stream, Http2Stream subtreeRoot) {
+    public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
     }
 
     @Override
-    public void goingAway() {
+    public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
+    }
+
+    @Override
+    public void onWeightChanged(Http2Stream stream, short oldWeight) {
     }
 }


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java
index 827f947a1dc..46596cd3f96 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java
@@ -20,10 +20,24 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyShort;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import io.netty.handler.codec.http2.Http2Stream.State;
 
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 /**
@@ -34,12 +48,16 @@ public class DefaultHttp2ConnectionTest {
     private DefaultHttp2Connection server;
     private DefaultHttp2Connection client;
 
+    @Mock
+    private Http2Connection.Listener clientListener;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
         server = new DefaultHttp2Connection(true);
         client = new DefaultHttp2Connection(false);
+        client.addListener(clientListener);
     }
 
     @Test(expected = Http2Exception.class)
@@ -258,6 +276,31 @@ public void insertExclusiveShouldAddNewLevel() throws Exception {
         assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
     }
 
+    @Test
+    public void weightChangeWithNoTreeChangeShouldNotifyListeners() throws Http2Exception {
+        Http2Stream streamA = client.local().createStream(1, false);
+        Http2Stream streamB = client.local().createStream(3, false);
+        Http2Stream streamC = client.local().createStream(5, false);
+        Http2Stream streamD = client.local().createStream(7, false);
+
+        streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        streamD.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
+
+        assertEquals(4, client.numActiveStreams());
+
+        short oldWeight = streamD.weight();
+        short newWeight = (short) (oldWeight + 1);
+        reset(clientListener);
+        streamD.setPriority(streamD.parent().id(), newWeight, false);
+        verify(clientListener).onWeightChanged(eq(streamD), eq(oldWeight));
+        Assert.assertEquals(streamD.weight(), newWeight);
+        verify(clientListener, never()).priorityTreeParentChanging(any(Http2Stream.class),
+                        any(Http2Stream.class));
+        verify(clientListener, never()).priorityTreeParentChanged(any(Http2Stream.class),
+                        any(Http2Stream.class));
+    }
+
     @Test
     public void removeShouldRestructureTree() throws Exception {
         Http2Stream streamA = client.local().createStream(1, false);
@@ -299,24 +342,55 @@ public void removeShouldRestructureTree() throws Exception {
 
     @Test
     public void circularDependencyShouldRestructureTree() throws Exception {
-        // Using example from http://tools.ietf.org/html/draft-ietf-httpbis-http2-12#section-5.3.3
+        // Using example from http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-5.3.3
+        // Initialize all the nodes
         Http2Stream streamA = client.local().createStream(1, false);
+        verifyParentChanged(streamA, null);
         Http2Stream streamB = client.local().createStream(3, false);
+        verifyParentChanged(streamB, null);
         Http2Stream streamC = client.local().createStream(5, false);
+        verifyParentChanged(streamC, null);
         Http2Stream streamD = client.local().createStream(7, false);
+        verifyParentChanged(streamD, null);
         Http2Stream streamE = client.local().createStream(9, false);
+        verifyParentChanged(streamE, null);
         Http2Stream streamF = client.local().createStream(11, false);
+        verifyParentChanged(streamF, null);
 
+        // Build the tree
         streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamB), anyShort());
+        verifyParentChanged(streamB, client.connectionStream());
+        verifyParentChanging(streamB, client.connectionStream());
+
         streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamC), anyShort());
+        verifyParentChanged(streamC, client.connectionStream());
+        verifyParentChanging(streamC, client.connectionStream());
+
         streamD.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamD), anyShort());
+        verifyParentChanged(streamD, client.connectionStream());
+        verifyParentChanging(streamD, client.connectionStream());
+
         streamE.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamE), anyShort());
+        verifyParentChanged(streamE, client.connectionStream());
+        verifyParentChanging(streamE, client.connectionStream());
+
         streamF.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamF), anyShort());
+        verifyParentChanged(streamF, client.connectionStream());
+        verifyParentChanging(streamF, client.connectionStream());
 
         assertEquals(6, client.numActiveStreams());
 
         // Non-exclusive re-prioritization of a->d.
+        reset(clientListener);
         streamA.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamA), anyShort());
+        verifyParentChanging(Arrays.asList(streamD, streamA), Arrays.asList(client.connectionStream(), streamD));
+        verifyParentsChanged(Arrays.asList(streamD, streamA), Arrays.asList(streamC, client.connectionStream()));
 
         // Level 0
         Http2Stream p = client.connectionStream();
@@ -358,26 +432,57 @@ public void circularDependencyShouldRestructureTree() throws Exception {
 
     @Test
     public void circularDependencyWithExclusiveShouldRestructureTree() throws Exception {
-        // Using example from http://tools.ietf.org/html/draft-ietf-httpbis-http2-12#section-5.3.3
-        // Although the expected output for the exclusive case has an error in the document. The
-        // final dependency of C should be E (not F). This is fixed here.
+        // Using example from http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-5.3.3
+        // Initialize all the nodes
         Http2Stream streamA = client.local().createStream(1, false);
+        verifyParentChanged(streamA, null);
         Http2Stream streamB = client.local().createStream(3, false);
+        verifyParentChanged(streamB, null);
         Http2Stream streamC = client.local().createStream(5, false);
+        verifyParentChanged(streamC, null);
         Http2Stream streamD = client.local().createStream(7, false);
+        verifyParentChanged(streamD, null);
         Http2Stream streamE = client.local().createStream(9, false);
+        verifyParentChanged(streamE, null);
         Http2Stream streamF = client.local().createStream(11, false);
+        verifyParentChanged(streamF, null);
 
+        // Build the tree
         streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamB), anyShort());
+        verifyParentChanged(streamB, client.connectionStream());
+        verifyParentChanging(streamB, client.connectionStream());
+
         streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamC), anyShort());
+        verifyParentChanged(streamC, client.connectionStream());
+        verifyParentChanging(streamC, client.connectionStream());
+
         streamD.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamD), anyShort());
+        verifyParentChanged(streamD, client.connectionStream());
+        verifyParentChanging(streamD, client.connectionStream());
+
         streamE.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamE), anyShort());
+        verifyParentChanged(streamE, client.connectionStream());
+        verifyParentChanging(streamE, client.connectionStream());
+
         streamF.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
+        verify(clientListener, never()).onWeightChanged(eq(streamF), anyShort());
+        verifyParentChanged(streamF, client.connectionStream());
+        verifyParentChanging(streamF, client.connectionStream());
 
         assertEquals(6, client.numActiveStreams());
 
         // Exclusive re-prioritization of a->d.
+        reset(clientListener);
         streamA.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, true);
+        verify(clientListener, never()).onWeightChanged(eq(streamA), anyShort());
+        verifyParentChanging(Arrays.asList(streamD, streamA, streamF),
+                             Arrays.asList(client.connectionStream(), streamD, streamA));
+        verifyParentsChanged(Arrays.asList(streamD, streamA, streamF),
+                             Arrays.asList(streamC, client.connectionStream(), streamD));
 
         // Level 0
         Http2Stream p = client.connectionStream();
@@ -416,4 +521,49 @@ public void circularDependencyWithExclusiveShouldRestructureTree() throws Except
         assertEquals(0, p.numChildren());
         assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
     }
+
+    private void verifyParentChanging(List<Http2Stream> expectedArg1, List<Http2Stream> expectedArg2) {
+        Assert.assertTrue(expectedArg1.size() == expectedArg2.size());
+        ArgumentCaptor<Http2Stream> arg1Captor = ArgumentCaptor.forClass(Http2Stream.class);
+        ArgumentCaptor<Http2Stream> arg2Captor = ArgumentCaptor.forClass(Http2Stream.class);
+        verify(clientListener, times(expectedArg1.size())).priorityTreeParentChanging(arg1Captor.capture(),
+                        arg2Captor.capture());
+        List<Http2Stream> capturedArg1 = arg1Captor.getAllValues();
+        List<Http2Stream> capturedArg2 = arg2Captor.getAllValues();
+        Assert.assertTrue(capturedArg1.size() == capturedArg2.size());
+        Assert.assertTrue(capturedArg1.size() == expectedArg1.size());
+        for (int i = 0; i < capturedArg1.size(); ++i) {
+            Assert.assertEquals(expectedArg1.get(i), capturedArg1.get(i));
+            Assert.assertEquals(expectedArg2.get(i), capturedArg2.get(i));
+        }
+    }
+
+    private void verifyParentsChanged(List<Http2Stream> expectedArg1, List<Http2Stream> expectedArg2) {
+        Assert.assertTrue(expectedArg1.size() == expectedArg2.size());
+        ArgumentCaptor<Http2Stream> arg1Captor = ArgumentCaptor.forClass(Http2Stream.class);
+        ArgumentCaptor<Http2Stream> arg2Captor = ArgumentCaptor.forClass(Http2Stream.class);
+        verify(clientListener, times(expectedArg1.size())).priorityTreeParentChanged(arg1Captor.capture(),
+                        arg2Captor.capture());
+        List<Http2Stream> capturedArg1 = arg1Captor.getAllValues();
+        List<Http2Stream> capturedArg2 = arg2Captor.getAllValues();
+        Assert.assertTrue(capturedArg1.size() == capturedArg2.size());
+        Assert.assertTrue(capturedArg1.size() == expectedArg1.size());
+        for (int i = 0; i < capturedArg1.size(); ++i) {
+            Assert.assertEquals(expectedArg1.get(i), capturedArg1.get(i));
+            Assert.assertEquals(expectedArg2.get(i), capturedArg2.get(i));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T streamEq(T stream) {
+        return (T) (stream == null ? isNull(Http2Stream.class) : eq(stream));
+    }
+
+    private void verifyParentChanging(Http2Stream stream, Http2Stream newParent) {
+        verify(clientListener).priorityTreeParentChanging(streamEq(stream), streamEq(newParent));
+    }
+
+    private void verifyParentChanged(Http2Stream stream, Http2Stream oldParent) {
+        verify(clientListener).priorityTreeParentChanged(streamEq(stream), streamEq(oldParent));
+    }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java
index efa647443e8..30930507844 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java
@@ -31,6 +31,12 @@
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.http2.DefaultHttp2OutboundFlowController.OutboundFlowState;
+import io.netty.util.collection.IntObjectHashMap;
+import io.netty.util.collection.IntObjectMap;
+
+import java.util.Arrays;
+import java.util.List;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,6 +52,7 @@ public class DefaultHttp2OutboundFlowControllerTest {
     private static final int STREAM_B = 3;
     private static final int STREAM_C = 5;
     private static final int STREAM_D = 7;
+    private static final int STREAM_E = 9;
 
     private DefaultHttp2OutboundFlowController controller;
 
@@ -193,8 +200,7 @@ public void connectionWindowUpdateShouldSendFrame() throws Http2Exception {
     @Test
     public void connectionWindowUpdateShouldSendPartialFrame() throws Http2Exception {
         // Set the connection window size to zero.
-        controller
-                .updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
 
         ByteBuf data = dummyData(10);
         send(STREAM_A, data);
@@ -252,8 +258,8 @@ public void streamWindowUpdateShouldSendPartialFrame() throws Http2Exception {
     }
 
     /**
-     * In this test, we block A which allows bytes to be written by C and D. Here's a view of the
-     * tree (stream A is blocked).
+     * In this test, we block A which allows bytes to be written by C and D. Here's a view of the tree (stream A is
+     * blocked).
      *
      * <pre>
      *         0
@@ -303,8 +309,8 @@ public void blockedStreamShouldSpreadDataToChildren() throws Http2Exception {
     }
 
     /**
-     * In this test, we block B which allows all bytes to be written by A. A should not share the
-     * data with its children since it's not blocked.
+     * In this test, we block B which allows all bytes to be written by A. A should not share the data with its children
+     * since it's not blocked.
      *
      * <pre>
      *         0
@@ -345,8 +351,8 @@ public void childrenShouldNotSendDataUntilParentBlocked() throws Http2Exception
     }
 
     /**
-     * In this test, we block B which allows all bytes to be written by A. Once A is blocked, it
-     * will spill over the remaining of its portion to its children.
+     * In this test, we block B which allows all bytes to be written by A. Once A is blocked, it will spill over the
+     * remaining of its portion to its children.
      *
      * <pre>
      *         0
@@ -405,8 +411,8 @@ public void parentShouldWaterFallDataToChildren() throws Http2Exception {
      *     C   D
      * </pre>
      *
-     * We then re-prioritize D so that it's directly off of the connection and verify that A and D
-     * split the written bytes between them.
+     * We then re-prioritize D so that it's directly off of the connection and verify that A and D split the written
+     * bytes between them.
      *
      * <pre>
      *           0
@@ -452,8 +458,8 @@ public void reprioritizeShouldAdjustOutboundFlow() throws Http2Exception {
     }
 
     /**
-     * In this test, we root all streams at the connection, and then verify that data is split
-     * appropriately based on weight (all available data is the same).
+     * In this test, we root all streams at the connection, and then verify that data is split appropriately based on
+     * weight (all available data is the same).
      *
      * <pre>
      *           0
@@ -515,8 +521,8 @@ public void writeShouldPreferHighestWeight() throws Http2Exception {
     }
 
     /**
-     * In this test, we root all streams at the connection, and then verify that data is split
-     * equally among the stream, since they all have the same weight.
+     * In this test, we root all streams at the connection, and then verify that data is split equally among the stream,
+     * since they all have the same weight.
      *
      * <pre>
      *           0
@@ -567,6 +573,277 @@ public void samePriorityShouldWriteEqualData() throws Http2Exception {
         assertEquals(333, dWritten);
     }
 
+    /**
+     * In this test, we block all streams and verify the priority bytes for each sub tree at each node are correct
+     *
+     * <pre>
+     *        [0]
+     *        / \
+     *       A   B
+     *      / \
+     *     C   D
+     * </pre>
+     */
+    @Test
+    public void subTreeBytesShouldBeCorrect() throws Http2Exception {
+        // Block the connection
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
+
+        Http2Stream stream0 = connection.connectionStream();
+        Http2Stream streamA = connection.stream(STREAM_A);
+        Http2Stream streamB = connection.stream(STREAM_B);
+        Http2Stream streamC = connection.stream(STREAM_C);
+        Http2Stream streamD = connection.stream(STREAM_D);
+
+        // Send a bunch of data on each stream.
+        IntObjectMap<Integer> streamSizes = new IntObjectHashMap<Integer>(4);
+        streamSizes.put(STREAM_A, 400);
+        streamSizes.put(STREAM_B, 500);
+        streamSizes.put(STREAM_C, 600);
+        streamSizes.put(STREAM_D, 700);
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        verifyNoWrite(STREAM_A);
+        verifyNoWrite(STREAM_B);
+        verifyNoWrite(STREAM_C);
+        verifyNoWrite(STREAM_D);
+
+        OutboundFlowState state = state(stream0);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamA);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_A, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamB);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_B)), state.priorityBytes());
+        state = state(streamC);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_C)), state.priorityBytes());
+        state = state(streamD);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_D)), state.priorityBytes());
+    }
+
+    /**
+     * In this test, we block all streams shift the priority tree and verify priority bytes for each subtree are correct
+     *
+     * <pre>
+     *        [0]
+     *        / \
+     *       A   B
+     *      / \
+     *     C   D
+     * </pre>
+     *
+     * After the tree shift:
+     * <pre>
+     *        [0]
+     *         |
+     *         A
+     *         |
+     *         B
+     *        / \
+     *       C   D
+     * </pre>
+     */
+    @Test
+    public void subTreeBytesShouldBeCorrectWithRestructure() throws Http2Exception {
+        // Block the connection
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
+
+        Http2Stream stream0 = connection.connectionStream();
+        Http2Stream streamA = connection.stream(STREAM_A);
+        Http2Stream streamB = connection.stream(STREAM_B);
+        Http2Stream streamC = connection.stream(STREAM_C);
+        Http2Stream streamD = connection.stream(STREAM_D);
+
+        // Send a bunch of data on each stream.
+        IntObjectMap<Integer> streamSizes = new IntObjectHashMap<Integer>(4);
+        streamSizes.put(STREAM_A, 400);
+        streamSizes.put(STREAM_B, 500);
+        streamSizes.put(STREAM_C, 600);
+        streamSizes.put(STREAM_D, 700);
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        verifyNoWrite(STREAM_A);
+        verifyNoWrite(STREAM_B);
+        verifyNoWrite(STREAM_C);
+        verifyNoWrite(STREAM_D);
+
+        streamB.setPriority(STREAM_A, DEFAULT_PRIORITY_WEIGHT, true);
+        OutboundFlowState state = state(stream0);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamA);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamB);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_B, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamC);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_C)), state.priorityBytes());
+        state = state(streamD);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_D)), state.priorityBytes());
+    }
+
+    /**
+     * In this test, we block all streams and add a node to the priority tree and verify
+     *
+     * <pre>
+     *        [0]
+     *        / \
+     *       A   B
+     *      / \
+     *     C   D
+     * </pre>
+     *
+     * After the tree shift:
+     * <pre>
+     *        [0]
+     *        / \
+     *       A   B
+     *       |
+     *       E
+     *      / \
+     *     C   D
+     * </pre>
+     */
+    @Test
+    public void subTreeBytesShouldBeCorrectWithAddition() throws Http2Exception {
+        // Block the connection
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
+
+        Http2Stream stream0 = connection.connectionStream();
+        Http2Stream streamA = connection.stream(STREAM_A);
+        Http2Stream streamB = connection.stream(STREAM_B);
+        Http2Stream streamC = connection.stream(STREAM_C);
+        Http2Stream streamD = connection.stream(STREAM_D);
+
+        Http2Stream streamE = connection.local().createStream(STREAM_E, false);
+        streamE.setPriority(STREAM_A, DEFAULT_PRIORITY_WEIGHT, true);
+
+        // Send a bunch of data on each stream.
+        IntObjectMap<Integer> streamSizes = new IntObjectHashMap<Integer>(4);
+        streamSizes.put(STREAM_A, 400);
+        streamSizes.put(STREAM_B, 500);
+        streamSizes.put(STREAM_C, 600);
+        streamSizes.put(STREAM_D, 700);
+        streamSizes.put(STREAM_E, 900);
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        send(STREAM_E, dummyData(streamSizes.get(STREAM_E)));
+        verifyNoWrite(STREAM_A);
+        verifyNoWrite(STREAM_B);
+        verifyNoWrite(STREAM_C);
+        verifyNoWrite(STREAM_D);
+        verifyNoWrite(STREAM_E);
+
+        OutboundFlowState state = state(stream0);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D, STREAM_E)), state.priorityBytes());
+        state = state(streamA);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_A, STREAM_E, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamB);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_B)), state.priorityBytes());
+        state = state(streamC);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_C)), state.priorityBytes());
+        state = state(streamD);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_D)), state.priorityBytes());
+        state = state(streamE);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_E, STREAM_C, STREAM_D)), state.priorityBytes());
+    }
+
+    /**
+     * In this test, we block all streams and remove a node from the priority tree and verify
+     *
+     * <pre>
+     *        [0]
+     *        / \
+     *       A   B
+     *      / \
+     *     C   D
+     * </pre>
+     *
+     * After the tree shift:
+     * <pre>
+     *        [0]
+     *       / | \
+     *      C  D  B
+     * </pre>
+     */
+    @Test
+    public void subTreeBytesShouldBeCorrectWithRemoval() throws Http2Exception {
+        // Block the connection
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
+
+        Http2Stream stream0 = connection.connectionStream();
+        Http2Stream streamA = connection.stream(STREAM_A);
+        Http2Stream streamB = connection.stream(STREAM_B);
+        Http2Stream streamC = connection.stream(STREAM_C);
+        Http2Stream streamD = connection.stream(STREAM_D);
+
+        // Send a bunch of data on each stream.
+        IntObjectMap<Integer> streamSizes = new IntObjectHashMap<Integer>(4);
+        streamSizes.put(STREAM_A, 400);
+        streamSizes.put(STREAM_B, 500);
+        streamSizes.put(STREAM_C, 600);
+        streamSizes.put(STREAM_D, 700);
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        verifyNoWrite(STREAM_A);
+        verifyNoWrite(STREAM_B);
+        verifyNoWrite(STREAM_C);
+        verifyNoWrite(STREAM_D);
+
+        streamA.close();
+
+        OutboundFlowState state = state(stream0);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_B, STREAM_C, STREAM_D)), state.priorityBytes());
+        state = state(streamA);
+        assertEquals(0, state.priorityBytes());
+        state = state(streamB);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_B)), state.priorityBytes());
+        state = state(streamC);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_C)), state.priorityBytes());
+        state = state(streamD);
+        assertEquals(calculateStreamSizeSum(streamSizes,
+                        Arrays.asList(STREAM_D)), state.priorityBytes());
+    }
+
+    private static OutboundFlowState state(Http2Stream stream) {
+        return (OutboundFlowState) stream.outboundFlow();
+    }
+
+    private static int calculateStreamSizeSum(IntObjectMap<Integer> streamSizes, List<Integer> streamIds) {
+        int sum = 0;
+        for (int i = 0; i < streamIds.size(); ++i) {
+            Integer streamSize = streamSizes.get(streamIds.get(i));
+            if (streamSize != null) {
+                sum += streamSize;
+            }
+        }
+        return sum;
+    }
+
     private void send(int streamId, ByteBuf data) throws Http2Exception {
         controller.writeData(ctx, streamId, data, 0, false, promise);
     }
@@ -576,16 +853,15 @@ private void verifyWrite(int streamId, ByteBuf data) {
     }
 
     private void verifyNoWrite(int streamId) {
-        verify(frameWriter, never()).writeData(eq(ctx), eq(streamId), any(ByteBuf.class), anyInt(),
-                anyBoolean(), eq(promise));
+        verify(frameWriter, never()).writeData(eq(ctx), eq(streamId), any(ByteBuf.class), anyInt(), anyBoolean(),
+                        eq(promise));
     }
 
     private void captureWrite(int streamId, ArgumentCaptor<ByteBuf> captor, boolean endStream) {
         verify(frameWriter).writeData(eq(ctx), eq(streamId), captor.capture(), eq(0), eq(endStream), eq(promise));
     }
 
-    private void setPriority(int stream, int parent, int weight, boolean exclusive)
-            throws Http2Exception {
+    private void setPriority(int stream, int parent, int weight, boolean exclusive) throws Http2Exception {
         connection.stream(stream).setPriority(parent, (short) weight, exclusive);
     }