229 lines
8.8 KiB
Python
229 lines
8.8 KiB
Python
from . import distribution
|
|
from . import geometry
|
|
from .distribution import Distribution
|
|
from .expr import Node
|
|
from .geometry import Point, Segment
|
|
from .quorum_system import Strategy
|
|
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, TypeVar
|
|
import collections
|
|
import matplotlib
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
def plot_node_load(filename: str,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None):
|
|
fig, ax = plt.subplots()
|
|
plot_node_load_on(ax, strategy, nodes, read_fraction, write_fraction)
|
|
ax.set_xlabel('Node')
|
|
ax.set_ylabel('Load')
|
|
fig.tight_layout()
|
|
fig.savefig(filename)
|
|
|
|
|
|
def plot_node_load_on(ax: plt.Axes,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None):
|
|
_plot_node_load_on(ax,
|
|
strategy,
|
|
nodes or list(strategy.nodes()),
|
|
scale=1,
|
|
scale_by_node_capacity=True,
|
|
read_fraction=read_fraction,
|
|
write_fraction=write_fraction)
|
|
|
|
|
|
def plot_node_utilization(filename: str,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None):
|
|
fig, ax = plt.subplots()
|
|
plot_node_utilization_on(ax, strategy, nodes, read_fraction, write_fraction)
|
|
ax.set_xlabel('Node')
|
|
ax.set_ylabel('Utilization')
|
|
fig.tight_layout()
|
|
fig.savefig(filename)
|
|
|
|
|
|
def plot_node_utilization_on(ax: plt.Axes,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None):
|
|
_plot_node_load_on(ax,
|
|
strategy,
|
|
nodes or list(strategy.nodes()),
|
|
scale=strategy.capacity(read_fraction, write_fraction),
|
|
scale_by_node_capacity=True,
|
|
read_fraction=read_fraction,
|
|
write_fraction=write_fraction)
|
|
|
|
|
|
def plot_node_throughput(filename: str,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None):
|
|
fig, ax = plt.subplots()
|
|
plot_node_throughput_on(ax, strategy, nodes, read_fraction, write_fraction)
|
|
ax.set_xlabel('Node')
|
|
ax.set_ylabel('Throughput')
|
|
fig.tight_layout()
|
|
fig.savefig(filename)
|
|
|
|
|
|
def plot_node_throughput_on(ax: plt.Axes,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None,
|
|
draw_node_capacities: bool = True):
|
|
nodes = nodes or list(strategy.nodes())
|
|
d = distribution.canonicalize_rw(read_fraction, write_fraction)
|
|
fr = sum(weight * fr for (fr, weight) in d.items())
|
|
fw = 1 - fr
|
|
# TODO(mwhittaker): Explain.
|
|
node_limits = [
|
|
fr * strategy.x_read_probability[node.x] / node_load +
|
|
fw * strategy.x_write_probability[node.x] / node_load
|
|
for node in nodes
|
|
for node_load in [strategy.node_load(
|
|
node,
|
|
read_fraction=read_fraction,
|
|
write_fraction=write_fraction
|
|
)]
|
|
]
|
|
_plot_node_load_on(ax,
|
|
strategy,
|
|
nodes,
|
|
scale=strategy.capacity(read_fraction, write_fraction),
|
|
scale_by_node_capacity=False,
|
|
read_fraction=read_fraction,
|
|
write_fraction=write_fraction,
|
|
node_limits=node_limits if draw_node_capacities else None)
|
|
|
|
|
|
def _plot_node_load_on(ax: plt.Axes,
|
|
sigma: Strategy[T],
|
|
nodes: List[Node[T]],
|
|
scale: float,
|
|
scale_by_node_capacity: bool,
|
|
read_fraction: Optional[Distribution] = None,
|
|
write_fraction: Optional[Distribution] = None,
|
|
node_limits: List[float] = None):
|
|
d = distribution.canonicalize_rw(read_fraction, write_fraction)
|
|
x_list = [node.x for node in nodes]
|
|
x_index = {x: i for (i, x) in enumerate(x_list)}
|
|
x_ticks = list(range(len(x_list)))
|
|
|
|
def one_hot(quorum: FrozenSet[T]) -> np.array:
|
|
bar_heights = np.zeros(len(x_list))
|
|
for x in quorum:
|
|
bar_heights[x_index[x]] = 1
|
|
return bar_heights
|
|
|
|
width = 0.8
|
|
def plot_quorums(sigma: Dict[FrozenSet[T], float],
|
|
fraction: float,
|
|
bottoms: np.array,
|
|
capacities: np.array,
|
|
cmap: matplotlib.colors.Colormap):
|
|
for (i, (quorum, weight)) in enumerate(sigma.items()):
|
|
bar_heights = scale * fraction * weight * one_hot(quorum)
|
|
if scale_by_node_capacity:
|
|
bar_heights /= capacities
|
|
|
|
ax.bar(x_ticks,
|
|
bar_heights,
|
|
bottom=bottoms,
|
|
color=cmap(0.75 - i * 0.5 / len(sigma)),
|
|
edgecolor='white', width=width)
|
|
|
|
for j, (bar_height, bottom) in enumerate(zip(bar_heights, bottoms)):
|
|
text = ''.join(str(x) for x in sorted(list(quorum))) # type: ignore
|
|
if bar_height != 0:
|
|
ax.text(x_ticks[j], bottom + bar_height / 2, text,
|
|
ha='center', va='center')
|
|
bottoms += bar_heights
|
|
|
|
# Plot the quorums.
|
|
fr = sum(weight * fr for (fr, weight) in d.items())
|
|
fw = 1 - fr
|
|
read_capacities = np.array([node.read_capacity for node in nodes])
|
|
write_capacities = np.array([node.write_capacity for node in nodes])
|
|
bottoms = np.zeros(len(x_list))
|
|
plot_quorums(sigma.sigma_r, fr, bottoms, read_capacities,
|
|
matplotlib.cm.get_cmap('Reds'))
|
|
plot_quorums(sigma.sigma_w, fw, bottoms,
|
|
write_capacities, matplotlib.cm.get_cmap('Blues'))
|
|
|
|
# Plot the limits, if there are any.
|
|
if node_limits is not None:
|
|
for (i, limit) in enumerate(node_limits):
|
|
ax.plot([i - width/2, i + width/2], [limit, limit], color='black')
|
|
|
|
ax.set_xticks(x_ticks)
|
|
ax.set_xticklabels(str(x) for x in x_list)
|
|
|
|
|
|
def plot_load_distribution(filename: str,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None):
|
|
fig, ax = plt.subplots()
|
|
plot_load_distribution_on(ax, strategy, nodes)
|
|
ax.set_xlabel('Read Fraction')
|
|
ax.set_ylabel('Load')
|
|
fig.tight_layout()
|
|
fig.savefig(filename)
|
|
|
|
|
|
def _group(segments: Dict[T, Segment]) -> Dict[Segment, List[T]]:
|
|
groups: Dict[Segment, List[T]] = collections.defaultdict(list)
|
|
for x, segment in segments.items():
|
|
matches = (s for s in groups if segment.approximately_equal(s))
|
|
groups[next(matches, segment)].append(x)
|
|
return groups
|
|
|
|
|
|
def plot_load_distribution_on(ax: plt.Axes,
|
|
strategy: Strategy[T],
|
|
nodes: Optional[List[Node[T]]] = None):
|
|
nodes = nodes or list(strategy.nodes())
|
|
|
|
# We want to plot every node's load distribution. Multiple nodes might
|
|
# have the same load distribution, so we group the nodes by their
|
|
# distribution. The grouping is a little annoying because two floats
|
|
# might not be exactly equal but pretty close.
|
|
groups = _group({
|
|
node.x: Segment(
|
|
Point(0, strategy.node_load(node, read_fraction=0)),
|
|
Point(1, strategy.node_load(node, read_fraction=1))
|
|
)
|
|
for node in nodes
|
|
})
|
|
|
|
# Compute and plot the max of all segments. We plot the load first so that
|
|
# it lies underneath the node loads.
|
|
path = geometry.max_of_segments(list(groups.keys()))
|
|
ax.plot([p[0] for p in path],
|
|
[p[1] for p in path],
|
|
label='load',
|
|
linewidth=4)
|
|
|
|
# We plot the node loads second so that they appear above the load.
|
|
for segment, xs in groups.items():
|
|
ax.plot([segment.l.x, segment.r.x],
|
|
[segment.l.y, segment.r.y],
|
|
'--',
|
|
label=','.join(str(x) for x in xs),
|
|
linewidth=2,
|
|
alpha=0.75)
|