// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef NET_SPDY_SPDY_PRIORITY_TREE_H_ #define NET_SPDY_SPDY_PRIORITY_TREE_H_ #include #include #include #include #include #include "base/basictypes.h" #include "base/containers/hash_tables.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" namespace net { // This data structure implements the HTTP2 prioritization data structure // defined in draft standard: // http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 // // Nodes can be added and removed, and dependencies between them defined. Each // node can have at most one parent and at most one child (forming a list), but // there can be multiple lists, with each list root having its own priority. // Individual nodes can also be marked as ready to read/write, and then the // whole structure can be queried to pick the next node to read/write out of // those ready. // // The NodeId type must be a POD that supports comparison (most // likely, it will be a number). namespace test { template class SpdyPriorityTreePeer; } const int kRootNodeId = 0; const int kDefaultWeight = 16; const int kMinWeight = 1; const int kMaxWeight = 256; template class SpdyPriorityTree { typedef std::vector > PriorityNodeList; public: SpdyPriorityTree(); ~SpdyPriorityTree(); typedef std::list List; struct Node { Node(); ~Node(); NodeId id; NodeId parent_id; int weight; // Weights can range between 1 and 256 (inclusive). // The total weight of this node's direct descendants. int total_child_weights; // The total weight of direct descendants that are writeable // (ready to write and not blocked). This value does not necessarily // reflect the current state of the tree; instead, we lazily update it // on calls to PropagateNodeState(node.id). int total_writeable_child_weights; List* child_list; // node ID's of children, if any bool blocked; // Is the associated stream write-blocked? bool ready; // Does the stream have data ready for writing? float priority; // The fraction of resources to dedicate to this node. }; // Orders in descending order of priority. struct NodePriorityComparator { bool operator ()(const std::pair& lhs, const std::pair& rhs); }; friend class test::SpdyPriorityTreePeer; // Return the number of nodes currently in the tree. int num_nodes() const; // Return true if the tree contains a node with the given ID. bool NodeExists(NodeId node_id) const; // Add a new node with the given weight and parent. Non-exclusive nodes // simply get added below the parent node. If exclusive = true, the node // becomes the parent's sole child and the parent's previous children // become the children of the new node. // Returns true on success. Returns false if the node already exists // in the tree, or if the parent node does not exist. bool AddNode(NodeId node_id, NodeId parent_id, int weight, bool exclusive); // Remove an existing node from the tree. Returns true on success, or // false if the node doesn't exist. bool RemoveNode(NodeId node_id); // Get the weight of the given node. int GetWeight(NodeId node_id) const; // Get the parent of the given node. If the node doesn't exist, or is a root // node (and thus has no parent), returns NodeId(). NodeId GetParent(NodeId node_id) const; // Get the child list of the given node. If the node doesn't exist, or has no // child, returns NULL. std::list* GetChildren(NodeId node_id) const; // Set the priority of the given node. bool SetWeight(NodeId node_id, int weight); // Set the parent of the given node. Returns true on success. // Returns false and has no effect if the node and/or the parent doesn't // exist. If the new parent is a descendant of the node (i.e. this would have // created a cycle) then we rearrange the topology of the tree as described // in the HTTP2 spec. bool SetParent(NodeId node_id, NodeId parent_id, bool exclusive); // Returns true if the node parent_id has child_id in its child_list. bool HasChild(NodeId parent_id, NodeId child_id) const; // Mark a node as blocked or unblocked. Return true on success, or false // if unable to mark the specified node. bool SetBlocked(NodeId node_id, bool blocked); // Mark whether or not a node is ready to write; i.e. whether there is // buffered data for the associated stream. Return true on success, or false // if unable to mark the specified node. bool SetReady(NodeId node_id, bool ready); // Return true if all internal invariants hold (useful for unit tests). // Unless there are bugs, this should always return true. bool ValidateInvariantsForTests() const; // Get the given node, or return NULL if it doesn't exist. const Node* FindNode(NodeId node_id) const; // Returns an ordered list of writeable nodes and their priorities. // Priority is calculated as: // parent's priority * (node's weight / sum of sibling weights) PriorityNodeList GetPriorityList(); protected: // Update the value of total_writeable_child_weights for the given node // to reflect the current state of the tree. void PropagateNodeState(NodeId node); private: typedef base::hash_map NodeMap; NodeMap all_nodes_; // maps from node IDs to Node objects DISALLOW_COPY_AND_ASSIGN(SpdyPriorityTree); }; template SpdyPriorityTree::SpdyPriorityTree() { Node* root_node = &all_nodes_[kRootNodeId]; root_node->id = kRootNodeId; root_node->weight = kDefaultWeight; root_node->parent_id = static_cast(kRootNodeId); root_node->child_list = new std::list; root_node->priority = 1.0; root_node->ready = true; } template SpdyPriorityTree::~SpdyPriorityTree() {} template SpdyPriorityTree::Node::Node() : parent_id(kRootNodeId), weight(kDefaultWeight), total_child_weights(0), total_writeable_child_weights(0), child_list(), blocked(false), ready(false), priority(0) { } template SpdyPriorityTree::Node::~Node() { delete child_list; } template bool SpdyPriorityTree::NodePriorityComparator::operator ()( const std::pair& lhs, const std::pair& rhs) { return lhs.second > rhs.second; } template int SpdyPriorityTree::num_nodes() const { return all_nodes_.size(); } template bool SpdyPriorityTree::NodeExists(NodeId node_id) const { return all_nodes_.count(node_id) != 0; } template bool SpdyPriorityTree::AddNode(NodeId node_id, NodeId parent_id, int weight, bool exclusive) { if (NodeExists(node_id) || !NodeExists(parent_id)) { return false; } if (weight < kMinWeight || weight > kMaxWeight) { return false; } Node* parent = &all_nodes_[parent_id]; Node* new_node = &all_nodes_[node_id]; new_node->id = node_id; new_node->weight = weight; new_node->parent_id = parent_id; if (exclusive) { // Move the parent's current children below the new node. new_node->child_list = parent->child_list; new_node->total_child_weights = parent->total_child_weights; // Update each child's parent_id. for (typename List::iterator it = new_node->child_list->begin(); it != new_node->child_list->end(); ++it) { Node* child = &all_nodes_[*it]; child->parent_id = node_id; } // Clear parent's old child data. parent->child_list = new std::list; parent->total_child_weights = 0; } else { new_node->child_list = new std::list; } // Add new node to parent. parent->child_list->push_back(node_id); parent->total_child_weights += weight; return true; } template bool SpdyPriorityTree::RemoveNode(NodeId node_id) { if (node_id == static_cast(kRootNodeId) || !NodeExists(node_id)) { return false; } const Node& node = all_nodes_[node_id]; DCHECK(NodeExists(node.parent_id)); Node* parent = &all_nodes_[node.parent_id]; // Remove the node id from parent's child list. parent->child_list->remove(node_id); parent->total_child_weights -= node.weight; // Move the node's children to the parent's child list. if (node.child_list != NULL) { // Update each child's parent_id and weight. for (typename List::iterator it = node.child_list->begin(); it != node.child_list->end(); ++it) { Node* child = &all_nodes_[*it]; child->parent_id = node.parent_id; // Divide the removed node's weight among its children, rounding to the // nearest valid weight. float float_weight = node.weight * static_cast(child->weight) / static_cast(node.total_child_weights); int new_weight = std::floor(float_weight + 0.5); if (new_weight == 0) { new_weight = 1; } child->weight = new_weight; parent->total_child_weights += child->weight; } parent->child_list->splice(parent->child_list->end(), *node.child_list); } // Delete the node. all_nodes_.erase(node_id); return true; } template int SpdyPriorityTree::GetWeight(NodeId node_id) const { const Node* node = FindNode(node_id); if (node != NULL) { return node->weight; } return 0; } template NodeId SpdyPriorityTree::GetParent(NodeId node_id) const { const Node* node = FindNode(node_id); if (node != NULL && node->id != static_cast(kRootNodeId)) { return node->parent_id; } return static_cast(kRootNodeId); } template std::list* SpdyPriorityTree::GetChildren(NodeId node_id) const { const Node* node = FindNode(node_id); if (node != NULL) { return node->child_list; } return NULL; } template bool SpdyPriorityTree::SetWeight( NodeId node_id, int weight) { if (!NodeExists(node_id)) { return false; } if (weight < kMinWeight || weight > kMaxWeight) { return false; } Node* node = &all_nodes_[node_id]; Node* parent = &all_nodes_[node->parent_id]; parent->total_child_weights += (weight - node->weight); node->weight = weight; return true; } template bool SpdyPriorityTree::SetParent( NodeId node_id, NodeId parent_id, bool exclusive) { if (!NodeExists(node_id) || !NodeExists(parent_id)) { return false; } if (node_id == parent_id) return false; Node* node = &all_nodes_[node_id]; Node* new_parent = &all_nodes_[parent_id]; // If the new parent is already the node's parent, we're done. if (node->parent_id == parent_id) { return true; } // Next, check to see if the new parent is currently a descendant // of the node. Node* last = new_parent; NodeId last_id = parent_id; bool cycle_exists = false; while (last->parent_id != static_cast(kRootNodeId)) { if (last->parent_id == node_id) { cycle_exists = true; break; } last_id = last->parent_id; DCHECK(NodeExists(last_id)); last = &all_nodes_[last_id]; } if (cycle_exists) { // The new parent moves to the level of the current node. SetParent(parent_id, node->parent_id, false); } // Remove node from old parent's child list. const NodeId old_parent_id = node->parent_id; DCHECK(NodeExists(old_parent_id)); Node* old_parent = &all_nodes_[old_parent_id]; old_parent->child_list->remove(node_id); old_parent->total_child_weights -= node->weight; // Make the change. node->parent_id = parent_id; new_parent->child_list->push_back(node_id); new_parent->total_child_weights += node->weight; return true; } template bool SpdyPriorityTree::SetBlocked(NodeId node_id, bool blocked) { if (!NodeExists(node_id)) { return false; } Node* node = &all_nodes_[node_id]; node->blocked = blocked; return true; } template bool SpdyPriorityTree::SetReady(NodeId node_id, bool ready) { if (!NodeExists(node_id)) { return false; } Node* node = &all_nodes_[node_id]; node->ready = ready; return true; } template void SpdyPriorityTree::PropagateNodeState(NodeId node_id) { // Reset total_writeable_child_weights to its maximum value. Node* node = &all_nodes_[node_id]; node->total_writeable_child_weights = node->total_child_weights; for (typename List::iterator it = node->child_list->begin(); it != node->child_list->end(); ++it) { PropagateNodeState(*it); } if (node->total_writeable_child_weights == 0 && (node->blocked || !node->ready)) { // Tell the parent that this entire subtree is unwriteable. Node* parent = &all_nodes_[node->parent_id]; parent->total_writeable_child_weights -= node->weight; } } template const typename SpdyPriorityTree::Node* SpdyPriorityTree::FindNode(NodeId node_id) const { typename NodeMap::const_iterator iter = all_nodes_.find(node_id); if (iter == all_nodes_.end()) { return NULL; } return &iter->second; } template bool SpdyPriorityTree::HasChild(NodeId parent_id, NodeId child_id) const { const Node* parent = FindNode(parent_id); return parent->child_list->end() != std::find(parent->child_list->begin(), parent->child_list->end(), child_id); } template std::vector > SpdyPriorityTree::GetPriorityList() { typedef std::pair PriorityNode; typedef std::vector PriorityList; PriorityList priority_list; // Update total_writeable_child_weights to reflect the current // state of the tree. PropagateNodeState(kRootNodeId); List queue; const Node* root_node = FindNode(kRootNodeId); DCHECK(root_node->priority == 1.0); // Start by examining our top-level nodes. for (typename List::iterator it = root_node->child_list->begin(); it != root_node->child_list->end(); ++it) { queue.push_back(*it); } while (!queue.empty()) { NodeId current_node_id = queue.front(); Node* current_node = &all_nodes_[current_node_id]; const Node* parent_node = FindNode(current_node->parent_id); if (current_node->blocked || !current_node->ready) { if (current_node->total_writeable_child_weights > 0) { // This node isn't writeable, but it has writeable children. // Calculate the total fraction of resources we can allot // to this subtree. current_node->priority = parent_node->priority * (static_cast(current_node->weight) / static_cast(parent_node->total_writeable_child_weights)); // Examine the children. for (typename List::iterator it = current_node->child_list->begin(); it != current_node->child_list->end(); ++it) { queue.push_back(*it); } } else { // There's nothing to see in this subtree. current_node->priority = 0; } } else { // This node is writeable; calculate its priority. current_node->priority = parent_node->priority * (static_cast(current_node->weight) / static_cast(parent_node->total_writeable_child_weights)); // Add this node to the priority list. priority_list.push_back(PriorityNode(current_node_id, current_node->priority)); } // Remove this node from the queue. queue.pop_front(); } // Sort the nodes in descending order of priority. std::sort(priority_list.begin(), priority_list.end(), NodePriorityComparator()); return priority_list; } template bool SpdyPriorityTree::ValidateInvariantsForTests() const { int total_nodes = 0; int nodes_visited = 0; // Iterate through all nodes in the map. for (typename NodeMap::const_iterator iter = all_nodes_.begin(); iter != all_nodes_.end(); ++iter) { ++total_nodes; ++nodes_visited; const Node& node = iter->second; // All nodes except the root should have a parent, and should appear in // the child_list of that parent. if (node.id != static_cast(kRootNodeId) && (!NodeExists(node.parent_id) || !HasChild(node.parent_id, node.id))) { DLOG(INFO) << "Parent node " << node.parent_id << " does not exist, or does not list node " << node.id << " as its child."; return false; } if (!node.child_list->empty()) { int total_child_weights = 0; // Iterate through the node's children. for (typename List::iterator it = node.child_list->begin(); it != node.child_list->end(); ++it) { ++nodes_visited; // Each node in the list should exist and should have this node // set as its parent. if (!NodeExists(*it) || node.id != GetParent(*it)) { DLOG(INFO) << "Child node " << *it << " does not exist, " << "or does not list " << node.id << " as its parent."; return false; } const Node* child = FindNode(*it); total_child_weights += child->weight; } // Verify that total_child_weights is correct. if (total_child_weights != node.total_child_weights) { DLOG(INFO) << "Child weight totals do not agree. For node " << node.id << " total_child_weights has value " << node.total_child_weights << ", expected " << total_child_weights; return false; } } } // Make sure num_nodes reflects the total number of nodes the map contains. if (total_nodes != num_nodes()) { DLOG(INFO) << "Map contains incorrect number of nodes."; return false; } // Validate the validation function; we should have visited each node twice // (except for the root) DCHECK(nodes_visited == 2*num_nodes() - 1); return true; } } // namespace net #endif // NET_SPDY_SPDY_PRIORITY_TREE_H_