After load-balancer was made capacity-aware it no longer equalizes tablet count per shard, but rather utilization of shard's storage. This makes the old presentation mode not useful in assessing whether balance was reached, since nodes with less capacity will get fewer tablets when in balanced state. This PR adds a new default presentation mode which scales tablet size by its storage utilization so that tablets which have equal shard utilization take equal space on the graph. To facilitate that, a new virtual table was added: system.load_per_node, which allows the tool to learn about load balancer's view on per-node capacity. It can also serve as a debugging interface to get a view of current balance according to the load-balancer. Closes scylladb/scylladb#23584 * github.com:scylladb/scylladb: tablet-mon.py: Add presentation mode which scales tablet size by its storage utilization tablet-mon.py: Center tablet id text properly in the vertical axis tablet-mon.py: Show migration stage tag in table mode only when migrating virtual-tables: Introduce system.load_per_node virtual_tables: memtable_filling_virtual_table: Propagate permit to execute() docs: virtual-tables: Fix instructions service: tablets: Keep load_stats inside tablet_allocator
778 lines
25 KiB
Python
Executable File
778 lines
25 KiB
Python
Executable File
#!/bin/env python
|
|
#
|
|
# Copyright (C) 2023-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
#
|
|
# This is a tool for live-monitoring the state of tablets and load balancing dynamics in a Scylla cluster.
|
|
#
|
|
# WARNING: This is not an officially supported tool. It is subject to arbitrary changes without notice.
|
|
#
|
|
# Usage:
|
|
#
|
|
# ./tablet-mon.py
|
|
#
|
|
# Tries to connect to a CQL server on localhost
|
|
#
|
|
# To connect to a different host, you can set up a tunnel:
|
|
#
|
|
# ssh -L *:9042:127.0.0.1:9042 -N <remote-host>
|
|
#
|
|
# Key bindings:
|
|
#
|
|
# t - toggle display of table tags. Each table has a unique color which fills tablets of that table.
|
|
# i - toggle display of tablet ids. Only visible when table tags are visible.
|
|
# s - toggle mode of scaling of tablet size. Modes:
|
|
# - fixed-size (each tablet has the same displayed size)
|
|
# - scaled-to-capacity (tablet size is proportional to its utilization of shard's storage capacity)
|
|
#
|
|
|
|
import math
|
|
import threading
|
|
import time
|
|
import logging
|
|
import pygame
|
|
|
|
from cassandra.cluster import Cluster
|
|
|
|
tablet_size_modes = ["fixed size", "scaled to capacity"]
|
|
tablet_size_mode_idx = 1
|
|
|
|
# Layout settings
|
|
tablet_size = 60
|
|
node_frame_size = 18
|
|
node_frame_thickness = 3
|
|
frame_size = 30
|
|
shard_spacing = 0
|
|
node_frame_mid = node_frame_size // 2
|
|
|
|
tablet_w = tablet_size
|
|
tablet_h = tablet_w // 2
|
|
tablet_radius = tablet_size // 7
|
|
tablet_frame_size = max(2, min(6, tablet_size // 14))
|
|
show_table_tag = False
|
|
show_tablet_id = False
|
|
|
|
# Animation settings
|
|
streaming_trace_duration_ms = 300
|
|
streaming_trace_decay_ms = 2000
|
|
streaming_done_trace_duration_ms = 40
|
|
streaming_done_trace_decay_ms = 1000
|
|
insert_time_ms = 500
|
|
fall_acceleration = 3000
|
|
fall_delay_ms = 0
|
|
|
|
# Displayed objects
|
|
nodes = []
|
|
nodes_by_id = {}
|
|
tracers = list()
|
|
trace_lines = set()
|
|
trace_lines_pool = list()
|
|
|
|
cql_update_timeout_ms = 800
|
|
cql_update_period = 0.2
|
|
|
|
|
|
class Node(object):
|
|
def __init__(self, id):
|
|
self.shards = []
|
|
self.id = id
|
|
self.capacity = None
|
|
self.capacity_weight = 1
|
|
|
|
|
|
class Shard(object):
|
|
def __init__(self):
|
|
self.tablets = {}
|
|
|
|
def ordered_tablets(self):
|
|
return sorted(self.tablets.values(), key=lambda t: t.id)
|
|
|
|
|
|
class Tablet(object):
|
|
STATE_NORMAL = 0
|
|
STATE_JOINING = 1
|
|
STATE_LEAVING = 2
|
|
|
|
def __init__(self, id, node, state, initial):
|
|
self.id = id
|
|
self.state = state
|
|
self.insert_time = pygame.time.get_ticks()
|
|
self.streaming = False
|
|
self.seq = 0 # Tablet index within table
|
|
|
|
table_id = id[0]
|
|
if table_id not in table_tag_colors:
|
|
table_tag_colors[table_id] = table_tag_palette[len(table_tag_colors) % len(table_tag_palette)]
|
|
self.table_tag_color = table_tag_colors[table_id]
|
|
|
|
# Updated by animate()
|
|
self.h = 0 # Height including tablet_frame_size
|
|
self.pos = 0 # Position from the bottom of shard column
|
|
self.size_frac = 0
|
|
self.speed = 0
|
|
self.fall_start = None
|
|
self.h_scale = 1
|
|
|
|
self.update_h_scale(node)
|
|
|
|
if initial:
|
|
self.h = self.h_scale * (tablet_h + tablet_frame_size * 2)
|
|
self.size_frac = 1
|
|
|
|
# Updated by redraw()
|
|
self.x = 0
|
|
self.y = 0
|
|
|
|
def update_h_scale(self, node):
|
|
if tablet_size_mode_idx == 0:
|
|
self.h_scale = 1
|
|
else:
|
|
self.h_scale = node.capacity_weight
|
|
|
|
def get_mid_x(self):
|
|
return self.x + tablet_frame_size + float(tablet_w) / 2
|
|
|
|
def get_mid_y(self):
|
|
return self.y + float(self.h) / 2
|
|
|
|
|
|
def cubic_ease_out(t):
|
|
# if t < 0.5:
|
|
# return 4 * t * t * t
|
|
# else:
|
|
# t -= 1
|
|
# return 1 + 4 * t * t * t
|
|
|
|
return t * t * t
|
|
|
|
def cubic_ease_in(t):
|
|
t = 1.0 - t
|
|
return 1.0 - t * t * t
|
|
|
|
|
|
class TraceLine(object):
|
|
"""
|
|
Fading line which is left behind by a Tracer
|
|
"""
|
|
|
|
def __init__(self, x, y, x2, y2, color, thickness, decay_ms, now):
|
|
self.x = x
|
|
self.y = y
|
|
self.x2 = x2
|
|
self.y2 = y2
|
|
self.color = color
|
|
self.thickness = thickness
|
|
self.start_time = now
|
|
self.decay_ms = decay_ms
|
|
|
|
# Surface position
|
|
self.sx = self.x - thickness/2
|
|
self.sy = self.y - thickness/2
|
|
|
|
# Convert to surface coordinates
|
|
self.x -= self.sx
|
|
self.y -= self.sy
|
|
self.x2 -= self.sx
|
|
self.y2 -= self.sy
|
|
|
|
# Translate to ensure positive coordinates within surface
|
|
if self.x2 < 0:
|
|
self.sx += self.x2
|
|
self.x -= self.x2
|
|
self.x2 = 0
|
|
|
|
if self.y2 < 0:
|
|
self.sy += self.y2
|
|
self.y -= self.y2
|
|
self.y2 = 0
|
|
|
|
size = (abs(int(self.x2 - self.x)) + thickness, abs(int(self.y2 - self.y)) + thickness)
|
|
self.surf = pygame.Surface(size)
|
|
self.surf.set_colorkey((0, 0, 0))
|
|
|
|
def draw(self, now):
|
|
time_left = self.decay_ms - (now - self.start_time)
|
|
if time_left <= 0:
|
|
return False
|
|
frac = cubic_ease_out(time_left / self.decay_ms)
|
|
alpha = int(250 * frac)
|
|
color = (self.color[0], self.color[1], self.color[2])
|
|
|
|
self.surf.set_alpha(alpha)
|
|
self.surf.fill((0, 0, 0))
|
|
# pygame.draw.circle(surf, color, (int(self.x2 + self.x)/2, int(self.y2 + self.y)/2), int(self.thickness/2 * frac))
|
|
pygame.draw.line(self.surf, color, (int(self.x), int(self.y)), (int(self.x2), int(self.y2)),
|
|
int(self.thickness * frac))
|
|
window.blit(self.surf, (self.sx, self.sy))
|
|
return True
|
|
|
|
|
|
def add_trace_line(x, y, x2, y2, color, thickness, decay_ms, now):
|
|
if trace_lines_pool:
|
|
t = trace_lines_pool.pop()
|
|
t.__init__(x, y, x2, y2, color, thickness, decay_ms, now)
|
|
trace_lines.add(t)
|
|
else:
|
|
trace_lines.add(TraceLine(x, y, x2, y2, color, thickness, decay_ms, now))
|
|
|
|
|
|
def draw_trace_lines(trace_lines, now):
|
|
global changed
|
|
for tl in list(trace_lines):
|
|
changed = True
|
|
if not tl.draw(now):
|
|
trace_lines_pool.append(tl)
|
|
trace_lines.remove(tl)
|
|
|
|
|
|
class HourGlass(object):
|
|
def __init__(self, now):
|
|
self.last_now = now
|
|
self.period_ms = 500
|
|
self.decay_ms = 300
|
|
self.trace_lines = []
|
|
self.x = None
|
|
self.y = None
|
|
|
|
def draw(self, now):
|
|
cx = window.get_size()[0] / 2
|
|
cy = window.get_size()[1] / 2
|
|
size = window.get_size()[0] / 5
|
|
|
|
new_x = math.sin((now % self.period_ms) / float(self.period_ms) * 2 * math.pi) * size / 2 + cx
|
|
new_y = math.cos((now % self.period_ms) / float(self.period_ms) * 2 * math.pi) * size / 2 + cy
|
|
|
|
if self.x:
|
|
self.trace_lines.append(TraceLine(self.x, self.y, new_x, new_y, (130, 130, 255), 30, self.decay_ms, now))
|
|
|
|
self.x = new_x
|
|
self.y = new_y
|
|
|
|
draw_trace_lines(self.trace_lines, now)
|
|
|
|
|
|
class Tracer(object):
|
|
"""
|
|
Shooting star which goes from one tablet to another
|
|
and leaves a fading trace line behind it
|
|
"""
|
|
|
|
def get_src_tablet(self):
|
|
return nodes_by_id[self.src_replica[0]].shards[self.src_replica[1]].tablets[self.tablet_id]
|
|
|
|
def get_dst_tablet(self):
|
|
return nodes_by_id[self.dst_replica[0]].shards[self.dst_replica[1]].tablets[self.tablet_id]
|
|
|
|
def __init__(self, tablet_id, src_replica, dst_replica, src_color, dst_color, thickness, decay_ms, duration_ms):
|
|
self.tablet_id = tablet_id
|
|
self.src_replica = src_replica
|
|
self.dst_replica = dst_replica
|
|
# Position can be calculated after first redraw() which lays out new tablets
|
|
self.x = None
|
|
self.y = None
|
|
self.end_time = None
|
|
self.last_now = None
|
|
|
|
self.decay_ms = decay_ms
|
|
self.duration_ms = duration_ms
|
|
self.src_color = src_color
|
|
self.dst_color = dst_color
|
|
self.thickness = thickness
|
|
|
|
def move(self, now):
|
|
def interpolate_color(frac, c1, c2):
|
|
return pygame.Color(
|
|
int(c1[0] + (c2[0] - c1[0]) * frac),
|
|
int(c1[1] + (c2[1] - c1[1]) * frac),
|
|
int(c1[2] + (c2[2] - c1[2]) * frac)
|
|
)
|
|
|
|
try:
|
|
dst_x = self.get_dst_tablet().get_mid_x()
|
|
dst_y = self.get_dst_tablet().get_mid_y()
|
|
|
|
if not self.last_now:
|
|
self.end_time = now + self.duration_ms
|
|
|
|
self.x = self.get_src_tablet().get_mid_x()
|
|
self.y = self.get_src_tablet().get_mid_y()
|
|
|
|
time_left = self.end_time - now
|
|
self.vx = (dst_x - self.x) / time_left
|
|
self.vy = (dst_y - self.y) / time_left
|
|
self.vy -= abs(self.vx) / 2 # Give higher vertical speed for arced trajectory
|
|
self.last_now = now
|
|
return True
|
|
|
|
if now == self.last_now:
|
|
return True
|
|
|
|
g_dt = float(now - self.last_now)
|
|
if now >= self.end_time:
|
|
g_dt = self.end_time - self.last_now
|
|
g_last_now = self.last_now
|
|
|
|
# Subdivide for smoother lines at high speeds
|
|
segment_length = 30
|
|
distance = math.sqrt((self.vx * g_dt) ** 2 + (self.vy * g_dt) ** 2)
|
|
n_segments = max(1, int(distance / segment_length))
|
|
|
|
for i in range(n_segments):
|
|
l_now = g_last_now + g_dt * (i + 1) / n_segments
|
|
dt = float(l_now - self.last_now)
|
|
|
|
time_left = self.end_time - self.last_now
|
|
vx = (dst_x - self.x) / time_left
|
|
vy = (dst_y - self.y) / time_left
|
|
|
|
steer_force = dt / time_left
|
|
self.vx = steer_force * vx + (1.0 - steer_force) * self.vx
|
|
self.vy = steer_force * vy + (1.0 - steer_force) * self.vy
|
|
|
|
frac = 1.0 - (self.end_time - self.last_now - dt) / self.duration_ms
|
|
new_x = self.x + self.vx * dt
|
|
new_y = self.y + self.vy * dt
|
|
|
|
add_trace_line(self.x, self.y, new_x, new_y,
|
|
interpolate_color(frac, self.src_color, self.dst_color), self.thickness, self.decay_ms, l_now)
|
|
|
|
self.x = new_x
|
|
self.y = new_y
|
|
self.last_now = l_now
|
|
|
|
return now < self.end_time
|
|
except KeyError: # src or dst disappeared
|
|
return False
|
|
|
|
|
|
def fire_tracer(tablet_id, src_replica, dst_replica, src_color, dst_color, thickness, decay_ms, duration_ms):
|
|
tracers.append(Tracer(tablet_id, src_replica, dst_replica, src_color, dst_color, thickness, decay_ms, duration_ms))
|
|
|
|
|
|
def move_tracers(now):
|
|
global changed
|
|
for tracer in list(tracers):
|
|
changed = True
|
|
if not tracer.move(now):
|
|
tracers.remove(tracer)
|
|
|
|
|
|
def on_capacity_weight_changed():
|
|
for node in nodes:
|
|
for shard in node.shards:
|
|
for tablet in shard.tablets.values():
|
|
tablet.update_h_scale(node)
|
|
changed = True
|
|
|
|
BLACK = (0, 0, 0)
|
|
WHITE = (255, 255, 255)
|
|
GRAY = (192, 192, 192)
|
|
light_yellow = (250, 240, 200)
|
|
even_darker_purple = (190, 180, 190)
|
|
dark_purple = (205, 195, 205)
|
|
light_purple = (240, 200, 255)
|
|
dark_red = (215, 195, 195)
|
|
dark_green = (195, 215, 195)
|
|
light_red = (255, 200, 200)
|
|
light_green = (200, 255, 200)
|
|
light_gray = (240, 240, 240)
|
|
|
|
tablet_colors = {
|
|
(Tablet.STATE_NORMAL, None): GRAY,
|
|
(Tablet.STATE_JOINING, 'allow_write_both_read_old'): dark_green,
|
|
(Tablet.STATE_LEAVING, 'allow_write_both_read_old'): dark_red,
|
|
(Tablet.STATE_JOINING, 'write_both_read_old'): dark_green,
|
|
(Tablet.STATE_LEAVING, 'write_both_read_old'): dark_red,
|
|
(Tablet.STATE_JOINING, 'streaming'): light_green,
|
|
(Tablet.STATE_LEAVING, 'streaming'): light_red,
|
|
(Tablet.STATE_JOINING, 'rebuild_repair'): light_green,
|
|
(Tablet.STATE_LEAVING, 'rebuild_repair'): light_red,
|
|
(Tablet.STATE_JOINING, 'write_both_read_new'): light_yellow,
|
|
(Tablet.STATE_LEAVING, 'write_both_read_new'): even_darker_purple,
|
|
(Tablet.STATE_JOINING, 'use_new'): light_yellow,
|
|
(Tablet.STATE_LEAVING, 'use_new'): dark_purple,
|
|
(Tablet.STATE_JOINING, 'cleanup'): light_yellow,
|
|
(Tablet.STATE_LEAVING, 'cleanup'): light_purple,
|
|
(Tablet.STATE_JOINING, 'end_migration'): light_yellow,
|
|
(Tablet.STATE_LEAVING, 'end_migration'): light_gray,
|
|
}
|
|
|
|
table_tag_palette = [
|
|
(238, 187, 187), # Light Red
|
|
(238, 221, 187), # Orange
|
|
(187, 238, 187), # Lime Green
|
|
(187, 238, 238), # Aqua
|
|
(187, 221, 238), # Light Blue
|
|
(221, 187, 238), # Lavender
|
|
(238, 187, 238), # Pink
|
|
(238, 238, 187), # Yellow
|
|
(238, 153, 153), # Lighter Red
|
|
(238, 187, 153), # Lighter Orange
|
|
(238, 238, 153), # Lighter Yellow
|
|
(153, 238, 153), # Lighter Green
|
|
(153, 221, 238), # Lighter Blue
|
|
(187, 153, 238), # Lighter Purple
|
|
(238, 153, 238), # Lighter Pink
|
|
(204, 204, 204) # Gray
|
|
]
|
|
|
|
# Map between table id and color
|
|
table_tag_colors = {}
|
|
|
|
# Avoid redrawing if nothing changed
|
|
changed = False
|
|
|
|
cluster = Cluster(['127.0.0.1'])
|
|
session = cluster.connect()
|
|
topo_query = session.prepare("SELECT host_id, shard_count FROM system.topology")
|
|
tablets_query = session.prepare("SELECT * FROM system.tablets")
|
|
load_per_node_query = session.prepare("SELECT * FROM system.load_per_node")
|
|
|
|
data_source_alive = False
|
|
|
|
def update_from_cql(initial=False):
|
|
"""
|
|
Updates objects using CQL queries of system state
|
|
|
|
:param initial: When True, disables animations which indicate changes.
|
|
We don't want initial state to appear as if everything suddenly changed.
|
|
"""
|
|
|
|
global changed
|
|
global data_source_alive
|
|
|
|
all_host_ids = set()
|
|
for host in session.execute(topo_query):
|
|
id = host.host_id
|
|
all_host_ids.add(id)
|
|
if id not in nodes_by_id:
|
|
n = Node(id)
|
|
nodes.append(n)
|
|
nodes_by_id[id] = n
|
|
changed = True
|
|
n = nodes_by_id[id]
|
|
if len(n.shards) > host.shard_count:
|
|
n.shards = n.shards[:host.shard_count]
|
|
while len(n.shards) < host.shard_count:
|
|
n.shards.append(Shard())
|
|
|
|
for id in nodes_by_id:
|
|
if id not in all_host_ids:
|
|
nodes.remove(nodes_by_id[id])
|
|
del nodes_by_id[id]
|
|
changed = True
|
|
|
|
for row in session.execute(load_per_node_query):
|
|
host_id = row.node
|
|
if host_id in nodes_by_id:
|
|
n = nodes_by_id[host_id]
|
|
if row.storage_capacity:
|
|
n.capacity = int(row.storage_capacity)
|
|
|
|
|
|
# Nodes are scaled to the node with smallest capacity.
|
|
# Nodes with more capacity will have smaller tablets (smaller capacity_weight).
|
|
min_capacity = min([n.capacity for n in nodes if n.capacity], default=0)
|
|
capacity_changed = False
|
|
for n in nodes:
|
|
if n.capacity:
|
|
new_weight = float(min_capacity) / n.capacity
|
|
if n.capacity_weight != new_weight:
|
|
n.capacity_weight = new_weight
|
|
capacity_changed = True
|
|
|
|
if capacity_changed:
|
|
on_capacity_weight_changed()
|
|
|
|
tablets_by_shard = set()
|
|
tablet_id_by_table = {}
|
|
|
|
def tablet_id_for_table(table_id):
|
|
if table_id not in tablet_id_by_table:
|
|
tablet_id_by_table[table_id] = 0
|
|
ret = tablet_id_by_table[table_id]
|
|
tablet_id_by_table[table_id] += 1
|
|
return ret
|
|
|
|
for tablet in session.execute(tablets_query):
|
|
tablet_seq = tablet_id_for_table(tablet.table_id)
|
|
id = (tablet.table_id, tablet.last_token)
|
|
replicas = set(tablet.replicas)
|
|
new_replicas = set(tablet.new_replicas) if tablet.new_replicas else replicas
|
|
|
|
leaving = replicas - new_replicas
|
|
joining = new_replicas - replicas
|
|
|
|
stage_change = False
|
|
inserted = False
|
|
for replica in replicas.union(new_replicas):
|
|
host = replica[0]
|
|
shard = replica[1]
|
|
|
|
if host not in nodes_by_id:
|
|
continue
|
|
|
|
tablets_by_shard.add((host, shard, id))
|
|
|
|
if replica in joining:
|
|
state = (Tablet.STATE_JOINING, tablet.stage)
|
|
elif replica in leaving:
|
|
state = (Tablet.STATE_LEAVING, tablet.stage)
|
|
else:
|
|
state = (Tablet.STATE_NORMAL, None)
|
|
|
|
node = nodes_by_id[host]
|
|
s = node.shards[shard]
|
|
if id not in s.tablets:
|
|
s.tablets[id] = Tablet(id, node, state, initial=initial)
|
|
stage_change = True
|
|
inserted = True
|
|
changed = True
|
|
t = s.tablets[id]
|
|
t.seq = tablet_seq
|
|
if t.state != state:
|
|
t.state = state
|
|
stage_change = True
|
|
changed = True
|
|
if not initial and t.streaming:
|
|
if tablet.stage != "streaming" and tablet.stage != "rebuild_repair" and tablet.stage != "write_both_read_old":
|
|
dst = t.streaming
|
|
t.streaming = None
|
|
fire_tracer(id, replica, dst, light_purple, light_yellow, tablet_h * 0.7,
|
|
streaming_done_trace_decay_ms, streaming_done_trace_duration_ms)
|
|
|
|
if not initial and stage_change and len(leaving) == 1 and len(joining) == 1:
|
|
src = leaving.pop()
|
|
dst = joining.pop()
|
|
src_tablet = nodes_by_id[src[0]].shards[src[1]].tablets[id]
|
|
if inserted:
|
|
src_tablet.streaming = dst
|
|
fire_tracer(id, src, dst, light_red, light_green, tablet_h,
|
|
streaming_trace_decay_ms, streaming_trace_duration_ms)
|
|
|
|
for n in nodes:
|
|
for s_idx, s in enumerate(n.shards):
|
|
for t in list(s.tablets.keys()):
|
|
if (n.id, s_idx, t) not in tablets_by_shard:
|
|
del s.tablets[t]
|
|
changed = True
|
|
|
|
|
|
def cql_updater():
|
|
global data_source_alive
|
|
while True:
|
|
try:
|
|
update_from_cql()
|
|
data_source_alive = True
|
|
except Exception as e:
|
|
print(e)
|
|
time.sleep(cql_update_period)
|
|
|
|
|
|
# Set the logging level to DEBUG for the Cassandra driver
|
|
cassandra_logger = logging.getLogger('cassandra')
|
|
cassandra_logger.setLevel(logging.DEBUG)
|
|
|
|
# Optionally, configure the logger to print DEBUG messages to the console
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(logging.DEBUG)
|
|
cassandra_logger.addHandler(console_handler)
|
|
|
|
update_from_cql(initial=True)
|
|
|
|
cassandra_thread = threading.Thread(target=cql_updater)
|
|
cassandra_thread.daemon = True
|
|
cassandra_thread.start()
|
|
|
|
pygame.init()
|
|
clock = pygame.time.Clock()
|
|
|
|
# Compute initial layout
|
|
max_tablet_count = 0
|
|
window_width = frame_size * 2
|
|
for node in nodes:
|
|
window_width += node_frame_size * 2
|
|
for shard in node.shards:
|
|
max_tablet_count = max(max_tablet_count, len(shard.tablets))
|
|
window_width += shard_spacing + tablet_w + tablet_frame_size * 2
|
|
|
|
window_height = frame_size * 2 + (tablet_h + tablet_frame_size * 2) * max_tablet_count + (node_frame_size * 2)
|
|
|
|
window_width = min(window_width, 3000)
|
|
window_height = min(window_height, 2000)
|
|
window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
|
|
pygame.display.set_caption('Tablets')
|
|
number_font = pygame.font.SysFont(None, 20)
|
|
|
|
def draw_tablet(tablet, x, y):
|
|
tablet.x = x
|
|
tablet.y = y
|
|
w = tablet.size_frac * tablet_w
|
|
h = tablet.h
|
|
if h > 2 * tablet_frame_size:
|
|
color = tablet_colors[tablet.state]
|
|
if show_table_tag:
|
|
table_tag_color = tablet.table_tag_color
|
|
else:
|
|
table_tag_color = color
|
|
|
|
pygame.draw.rect(window, table_tag_color, (x + tablet_frame_size + (tablet_w - w) / 2,
|
|
y + tablet_frame_size,
|
|
w,
|
|
h - 2 * tablet_frame_size), border_radius=tablet_radius)
|
|
|
|
if show_table_tag and tablet.state[0] != Tablet.STATE_NORMAL:
|
|
table_tag_h = tablet_radius
|
|
pygame.draw.rect(window, color, (x + tablet_frame_size + (tablet_w - w) / 2,
|
|
y + tablet_frame_size,
|
|
w,
|
|
table_tag_h),
|
|
border_top_left_radius=tablet_radius,
|
|
border_top_right_radius=tablet_radius)
|
|
|
|
if show_table_tag and show_tablet_id:
|
|
number_text = str(tablet.seq)
|
|
number_image = number_font.render(number_text, True, BLACK)
|
|
window.blit(number_image, (x + tablet_frame_size + (w - number_image.get_width()) / 2,
|
|
y + tablet_frame_size + (h - 2 * tablet_frame_size - number_image.get_height()) / 2))
|
|
|
|
def draw_node_frame(x, y, x2, y2, color):
|
|
pygame.draw.rect(window, color, (x, y, x2 - x, y2 - y), node_frame_thickness,
|
|
border_radius=tablet_radius + tablet_frame_size + node_frame_mid)
|
|
|
|
def animate(now):
|
|
global changed
|
|
|
|
tablet_max_h = tablet_h + 2 * tablet_frame_size
|
|
|
|
def interpolate(start, duration):
|
|
return min(1.0, (now - start) / duration)
|
|
|
|
for node in nodes:
|
|
for shard in node.shards:
|
|
tablet_max_pos = 0
|
|
for tablet in shard.ordered_tablets():
|
|
# Appearing
|
|
if tablet.h < tablet.h_scale * tablet_max_h:
|
|
frac = cubic_ease_in(interpolate(tablet.insert_time, insert_time_ms))
|
|
new_h = frac * tablet.h_scale * tablet_max_h
|
|
tablet.pos += new_h - tablet.h
|
|
tablet.h = new_h
|
|
tablet.size_frac = frac
|
|
changed = True
|
|
elif tablet.h > tablet_max_h * tablet.h_scale:
|
|
tablet.h = tablet_max_h * tablet.h_scale
|
|
tablet.size_frac = 1
|
|
changed = True
|
|
|
|
# Falling
|
|
if tablet.pos > tablet_max_pos + tablet.h:
|
|
if not tablet.fall_start:
|
|
tablet.fall_start = now
|
|
tablet.speed = 0
|
|
tablet.fall_real_start = now + fall_delay_ms
|
|
tablet.last_now = tablet.fall_real_start
|
|
|
|
if now > tablet.fall_real_start:
|
|
dt = (now - tablet.last_now) / 1000 # Convert to seconds
|
|
tablet.pos -= tablet.speed * dt + fall_acceleration * dt * dt / 2
|
|
tablet.speed += fall_acceleration * dt
|
|
tablet.last_now = now
|
|
changed = True
|
|
|
|
# Bouncing
|
|
if tablet.pos < tablet_max_pos + tablet.h:
|
|
tablet.pos = tablet_max_pos + tablet.h
|
|
tablet.fall_start = None
|
|
changed = True
|
|
|
|
tablet_max_pos = tablet.pos
|
|
|
|
|
|
def redraw():
|
|
"""
|
|
Calculates final layout of objects and draws them
|
|
"""
|
|
|
|
background_color = WHITE
|
|
window.fill(background_color)
|
|
|
|
node_x = frame_size
|
|
node_y = frame_size
|
|
|
|
for node in nodes:
|
|
bottom_line = window_height - frame_size
|
|
|
|
draw_node_frame(node_x + node_frame_mid,
|
|
node_y + node_frame_mid,
|
|
node_x + 2 * node_frame_size + len(node.shards) * (tablet_w + tablet_frame_size * 2) - node_frame_mid,
|
|
bottom_line - node_frame_mid,
|
|
GRAY)
|
|
|
|
node_x += node_frame_size
|
|
|
|
for shard in node.shards:
|
|
for tablet in shard.ordered_tablets():
|
|
tablet_y = bottom_line - node_frame_size - tablet.pos
|
|
draw_tablet(tablet, node_x, tablet_y)
|
|
|
|
node_x += tablet_frame_size * 2 + tablet_w
|
|
|
|
node_x += node_frame_size
|
|
|
|
redraw()
|
|
|
|
last_update = pygame.time.get_ticks()
|
|
hour_glass = HourGlass(pygame.time.get_ticks())
|
|
|
|
running = True
|
|
while running:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
running = False
|
|
elif event.type == pygame.VIDEORESIZE:
|
|
window_width, window_height = event.w, event.h
|
|
changed = True
|
|
elif event.type == pygame.KEYDOWN:
|
|
if event.key == pygame.K_t:
|
|
show_table_tag = not show_table_tag
|
|
changed = True
|
|
elif event.key == pygame.K_i:
|
|
show_tablet_id = not show_tablet_id
|
|
changed = True
|
|
elif event.key == pygame.K_s:
|
|
tablet_size_mode_idx = (tablet_size_mode_idx + 1) % len(tablet_size_modes)
|
|
print("Tablet size mode:", tablet_size_modes[tablet_size_mode_idx])
|
|
on_capacity_weight_changed()
|
|
|
|
now = float(pygame.time.get_ticks())
|
|
|
|
animate(now)
|
|
|
|
if changed:
|
|
changed = False
|
|
redraw()
|
|
|
|
now = pygame.time.get_ticks()
|
|
move_tracers(now)
|
|
draw_trace_lines(trace_lines, now)
|
|
|
|
# Check connectivity with data source
|
|
# and display a pane while reconnecting
|
|
if data_source_alive:
|
|
last_update = now
|
|
data_source_alive = False
|
|
elif now - last_update > cql_update_timeout_ms:
|
|
surf = pygame.Surface(window.get_size())
|
|
surf.fill(WHITE)
|
|
surf.set_alpha(200)
|
|
window.blit(surf, (0, 0))
|
|
hour_glass.draw(now)
|
|
changed = True
|
|
|
|
pygame.display.flip()
|
|
clock.tick(100)
|
|
|
|
pygame.quit()
|