Opsmas 2025 Day 5: ring & actionboard
TOC:
ring
into every developer’s life two things happen: they discover then fall in love with bloom filters and then the process repeats again when discovering the lmax disruptor.
we already have bloom filters over in datakit. now, here, now, we present: ring a high-performance C lmax-disruptor style producer/consumer ring buffer.
You know what a distruptor is already. It’s a contiguous memory allocation treated as a ring buffer with indepdnent producer and consumer indexes.
A typical ring buffer is used for things like circular logging where you want to keep recent logs without needing to manage storage or capacity yourself, so you make a 1MB log memory buffer and overwrite old logs with new logs in a ring when the buffer is full. The ring buffer/disruptor pattern adds one more superpower though: a disruptor ring buffer is also aware of how far consumers using the ring buffer have “consumed” into the current “ring buffer ring index” which easily prevents producer “next writes” from erasing “unread” entries across all consumers. feature packed power happening in a single block of memory with no locks to achieve, ideally, safely broadcasting atomic low latency messages/updates at 50+ GB/s inside a large multi-core server (or tiny multi-core laptop).
+---------------------------------------------------------------------+
| Ring Buffer |
| +-----+-----+-----+-----+-----+-----+-----+-----+ |
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | (power of 2) |
| +-----+-----+-----+-----+-----+-----+-----+-----+ |
| ^ ^ ^ |
| | | | |
| slowestConsumer consumer publisher |
| cursor cursor writeCursor |
+---------------------------------------------------------------------+
Producer writes at 'publisherWriteCursor', advances cursor
Consumers read up to 'consumerBarrier[N]', advance their cursors
Ring wraps around using bitwise AND (cursor & (size-1))The primary benefit of using distruptor/mpmc ring buffers with participant cursor indexes is you avoid “big fat heavy” kernel/syscall locking which can be just slow and heavy enough in some cases to impact your performance advantage (not to mention things like pthread mutexes are insanely “big” and blow cache lines out of the water).
The interesting part is when you apply these practices to massively multi-core machines with shared memory because then every core can read and write to the same non-blocking memory space as fast as possible (thanks to the magic of modern processors and multi-word atomic store/load instructions) so all participants in the ring buffer all have their hands in the same memory bucket with no external conflicts.
My adaptation of the system here is interesting (i think) because I tried to allow the system to be shaped in any way feels natural for your own program to work. You can define the size/shape/content of the ring buffer slots then import the ring library and it’ll use your definitions instead of the default slot configuration (default is just every slot being a void * pointer):
typedef struct {
uint64_t timestamp;
uint32_t metricId;
double value;
} MetricSample;
#define RING_CONTENT_TYPE MetricSample
#include "ringComplete.h"
ring *metricsRing = ringNew(1, 1, 65536);
// Called from ANY thread - lock-free!
void recordMetric(uint32_t id, double value) {
ringCursorIdx cursor;
if (!ringPublisherMultiEntryGetNext(metricsRing, &cursor)) {
return; // Drop if full
}
MetricSample *s = &ringEntryWrite(metricsRing, cursor)->content;
s->timestamp = getTimestamp();
s->metricId = id;
s->value = value;
ringPublisherMultiEntryCommit(metricsRing, cursor, 0);
}You can see the ring system is also designed to be completely isolated/static/interned into your own program so you get the benefit of native compiler single-unit optimizations and inling and any other auto-vectorization or cpu-level speedups all where you need high performance interfaces the most.
Using multiple readers for one writer allows you to have one ‘primary data poster’ then other threads can have individual goals like “write to disk” or “replicate across network” from the same shared and protected high performance ring buffer system.
typedef struct {
uint64_t txnId;
uint64_t lsn;
void *record;
size_t recordLen;
} WALEntry;
#define RING_CONTENT_TYPE WALEntry
#include "ringComplete.h"
// Two-stage: Transaction -> WAL Writer -> Replication
ring *walRing = ringNew(2, 2, 32768);
void *walWriter(void *arg) {
ringCursorIdx cursor;
ringConsumerIdx id;
ringConsumerRegister(walRing, &cursor, &id);
while (running) {
ringCursorIdx upper = cursor;
if (!ringConsumerBarrierGetNext(walRing, &upper, 0)) {
usleep(100);
continue;
}
// Batch write entries to disk
for (ringCursorIdx i = cursor; i <= upper; i++) {
const WALEntry *e = &ringEntryRead(walRing, i)->content;
writeToWAL(e);
}
fsync(walFd);
// Signal replication can proceed (barrier 1)
ringPublisherEntryCommit(walRing, upper, 1);
ringConsumerBarrierReleaseEntry(walRing, id, upper);
cursor = upper + 1;
}
return NULL;
}anyway, here’s some metrics (m1 macbook pro)
================================================================================
RING BUFFER PERFORMANCE REPORT
================================================================================
Configuration: 3 runs, 1000 ms duration per run
Platform: macOS
Architecture: ARM64
--------------------------------------------------------------------------------
THROUGHPUT TESTS
--------------------------------------------------------------------------------
Single Producer, Single Consumer (small ring) ...
Throughput: 10.91 M ops/sec (stddev: 4.66%)
Latency avg: 63.95 ns
Latency range: 60.15 - 67.58 ns
Memory: 8960 bytes (8.75 KB)
Single Producer, Single Consumer (medium ring) ...
Throughput: 11.58 M ops/sec (stddev: 2.53%)
Latency avg: 62.64 ns
Latency range: 60.07 - 64.69 ns
Memory: 131840 bytes (128.75 KB)
Single Producer, Single Consumer (large ring) ...
Throughput: 10.98 M ops/sec (stddev: 4.62%)
Latency avg: 67.67 ns
Latency range: 62.74 - 72.24 ns
Memory: 1049344 bytes (1024.75 KB)
Single Producer, Single Consumer (larger ring) ...
Throughput: 11.16 M ops/sec (stddev: 3.08%)
Latency avg: 65.97 ns
Latency range: 62.98 - 68.75 ns
Memory: 4195072 bytes (4096.75 KB)
Single Producer, Multi Consumer (2) ...
Throughput: 12.15 M ops/sec (stddev: 2.81%)
Latency avg: 58.74 ns
Latency range: 56.22 - 61.87 ns
Memory: 2098048 bytes (2048.88 KB)
Single Producer, Multi Consumer (4) ...
Throughput: 12.18 M ops/sec (stddev: 5.41%)
Latency avg: 57.59 ns
Latency range: 54.54 - 63.49 ns
Memory: 2098304 bytes (2049.12 KB)
Multi Producer (2), Single Consumer ...
Throughput: 12.36 M ops/sec (stddev: 2.37%)
Latency avg: 137.31 ns
Latency range: 134.07 - 142.76 ns
Memory: 2097920 bytes (2048.75 KB)
Multi Producer (4), Single Consumer ...
Throughput: 0.26 M ops/sec (stddev: 2.14%)
Latency avg: 14916.59 ns
Latency range: 14678.66 - 15373.54 ns
Memory: 2097920 bytes (2048.75 KB)
Multi Producer (2), Multi Consumer (2) ...
Throughput: 8.89 M ops/sec (stddev: 13.45%)
Latency avg: 203.99 ns
Latency range: 179.11 - 251.38 ns
Memory: 2098048 bytes (2048.88 KB)
Multi Producer (4), Multi Consumer (4) ...
Throughput: 0.17 M ops/sec (stddev: 11.64%)
Latency avg: 23624.80 ns
Latency range: 20022.60 - 25708.30 ns
Memory: 2098304 bytes (2049.12 KB)
--------------------------------------------------------------------------------
SCALABILITY TESTS (Ring Size Impact)
--------------------------------------------------------------------------------
Ring size: 16 entries ...
Throughput: 6.21 M ops/sec (stddev: 2.54%)
Latency avg: 28.70 ns
Latency range: 28.02 - 29.68 ns
Memory: 2816 bytes (2.75 KB)
Ring size: 64 entries ...
Throughput: 11.24 M ops/sec (stddev: 1.33%)
Latency avg: 62.27 ns
Latency range: 61.00 - 63.07 ns
Memory: 8960 bytes (8.75 KB)
Ring size: 256 entries ...
Throughput: 10.97 M ops/sec (stddev: 2.44%)
Latency avg: 66.68 ns
Latency range: 64.25 - 68.92 ns
Memory: 33536 bytes (32.75 KB)
Ring size: 1024 entries ...
Throughput: 11.56 M ops/sec (stddev: 2.12%)
Latency avg: 63.28 ns
Latency range: 60.92 - 65.16 ns
Memory: 131840 bytes (128.75 KB)
Ring size: 4096 entries ...
Throughput: 11.32 M ops/sec (stddev: 3.01%)
Latency avg: 64.92 ns
Latency range: 62.33 - 68.50 ns
Memory: 525056 bytes (512.75 KB)
Ring size: 16384 entries ...
Throughput: 11.13 M ops/sec (stddev: 2.02%)
Latency avg: 66.37 ns
Latency range: 64.73 - 68.78 ns
Memory: 2097920 bytes (2,048.75 KB)
Ring size: 262,144 entries ...
Throughput: 10.88 M ops/sec (stddev: 3.49%)
Latency avg: 68.27 ns
Latency range: 65.77 - 72.71 ns
Memory: 33555200 bytes (32,768.75 KB)
Ring size: 1,048,576 entries ...
Throughput: 10.79 M ops/sec (stddev: 8.36%)
Latency avg: 69.63 ns
Latency range: 63.78 - 80.75 ns
Memory: 134218496 bytes (131,072.75 KB)
Ring size: 33,554,432 entries ...
Throughput: 11.93 M ops/sec (stddev: 1.86%)
Latency avg: 60.37 ns
Latency range: 58.66 - 62.29 ns
Memory: 4294968064 bytes (4,194,304.75 KB)
--------------------------------------------------------------------------------
CONTENTION TESTS (Producer Scaling)
--------------------------------------------------------------------------------
1 producer(s), 1 consumer ...
Throughput: 11.80 M ops/sec (stddev: 1.08%)
Latency avg: 61.47 ns
Latency range: 60.21 - 62.13 ns
Memory: 134218496 bytes (131072.75 KB)
2 producer(s), 1 consumer ...
Throughput: 8.56 M ops/sec (stddev: 12.90%)
Latency avg: 211.77 ns
Latency range: 173.10 - 240.58 ns
Memory: 134218496 bytes (131072.75 KB)
4 producer(s), 1 consumer ...
Throughput: 0.25 M ops/sec (stddev: 3.70%)
Latency avg: 15574.92 ns
Latency range: 14775.25 - 16011.19 ns
Memory: 134218496 bytes (131072.75 KB)
8 producer(s), 1 consumer ...
Throughput: 0.15 M ops/sec (stddev: 16.82%)
Latency avg: 55969.88 ns
Latency range: 47934.67 - 71041.73 ns
Memory: 134218496 bytes (131072.75 KB)
--------------------------------------------------------------------------------
MEMORY EFFICIENCY
--------------------------------------------------------------------------------
Ring Size vs Memory Usage:
Entries Memory (bytes) Bytes/Entry Overhead
--------------------------------------------------------------------------------
16 2816 176.00 112.00
64 8960 140.00 76.00
256 33536 131.00 67.00
1024 131840 128.75 64.75
4096 525056 128.19 64.19
16384 2097920 128.05 64.05
32768 4195072 128.02 64.02
65536 8389376 128.01 64.01
1048576 134218496 128.00 64.00
33554432 4294968064 128.00 64.00
--------------------------------------------------------------------------------
SUMMARY
--------------------------------------------------------------------------------
Best throughput configuration:
Single Producer, Multi Consumer (2): 12.29 M ops/sec
================================================================================
END OF PERFORMANCE REPORT
================================================================================stats
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
C 5 4664 3286 612 766
C Header 6 448 322 52 74
CMake 2 225 183 15 27
===============================================================================
Total 13 5337 3791 679 867
===============================================================================actionboard
I was looking into stream deck things earlier this year, but even just a slightly outdated iPad should be able to do the same thing without need a dedicated “button machine.”
so i had code robot whip one up for me: actionboard
You can run it with:
git clone https://github.com/mattsta/actionboard
cd actionboard
uv sync -U
# launch the web app:
uv run uvicorn src.visual_control_board.main:app --reload --host 0.0.0.0 --port 8000
# then in another tab, you can also launch the advanced demo (connects into the already running app to update buttons live):
uv run python -m examples.dynamic_board_controlleraction board is fully configurable for any python action. actionboard includes an API where it can be configured dynamically live while running (redundant?) for adding buttons, updating button images and text, and even showing live charts/sparklines inside buttons.
configs look like:
simple action configs
actions:
- id: "greet_user_action"
module: "visual_control_board.actions.built_in_actions"
function: "greet_user_action"
- id: "log_current_time_action"
module: "visual_control_board.actions.built_in_actions"
function: "log_current_time_action"ui design configs
each outer key here is a “tab” on the page with its own icon/button/feature set
pages:
- name: "Main Controls"
id: "main_page"
layout: "grid"
grid_columns: 3
buttons:
- id: "greet_dev_button"
text: "Greet Developer"
icon_class: "fas fa-hand-sparkles"
action_id: "greet_user_action"
action_params:
name: "Developer"
style_class: "button-primary"
- id: "log_time_button"
text: "Log Time"
icon_class: "fas fa-clock"
action_id: "log_current_time_action"
style_class: "button-secondary"
- id: "async_action_button"
text: "Async Task (2s)"
icon_class: "fas fa-cogs"
action_id: "example_async_action"
action_params:
duration: 2
style_class: "button-success"
- id: "another_action_button"
text: "Generic Action"
icon_class: "fas fa-star"
action_id: "another_action"
style_class: "button-danger"
- id: "missing_action_button"
text: "Test Missing Action"
icon_class: "fas fa-question-circle"
action_id: "non_existent_action"
- id: "greet_world_button"
text: "Greet World"
icon_class: "fas fa-globe"
action_id: "greet_user_action"
action_params:
name: "World"
- name: "System Utilities"
id: "system_utils_page"
layout: "grid"
grid_columns: 2
buttons:
- id: "sys_log_time_button"
text: "Log Time (Sys)"
icon_class: "fas fa-hourglass-half"
action_id: "log_current_time_action"
style_class: "button-secondary"
- id: "sys_greet_admin_button"
text: "Greet Admin"
icon_class: "fas fa-user-shield"
action_id: "greet_user_action"
action_params:
name: "Administrator"
style_class: "button-primary"and “actions” are basically python functions doing anything a python function can do:
def greet_user_action(name: str = "User"):
"""
A simple synchronous action that logs a greeting and returns a message.
This action demonstrates how parameters from `action_params` in the UI config
can be passed to an action function.
Args:
name (str): The name of the user to greet. Defaults to "User".
Returns:
dict: A dictionary containing the status and a greeting message.
This dictionary is often used to provide feedback to the UI.
"""
message = f"Hello, {name}! This greeting action was successfully triggered."
logger.info(f"Executing greet_user_action for '{name}'")
# The returned dictionary can be used by the frontend (e.g., to show a toast message)
return {"status": "success", "message": message}
def log_current_time_action():
"""
Logs the current ISO formatted time to the server console and returns it
along with a success message.
Returns:
dict: A dictionary containing the status, current timestamp, and a message.
"""
now = datetime.datetime.now().isoformat()
message = f"Current server time: {now}"
logger.info(f"Executing log_current_time_action. Time: {now}")
return {"status": "success", "timestamp": now, "message": message}
async def example_async_action(duration: int = 1):
"""
An example of an asynchronous action that simulates a delay (e.g., a long-running task).
Args:
duration (int): The number of seconds to wait. Defaults to 1.
Returns:
dict: A dictionary containing the status, a completion message, and the duration.
"""
message_start = f"Starting async action (simulated duration: {duration}s)..."
logger.info(message_start)
await asyncio.sleep(duration) # Simulate an I/O bound operation
message_end = f"Async action completed after {duration}s."
logger.info(message_end)
return {"status": "success", "message": message_end, "duration": duration}
def another_action():
"""
Another simple placeholder synchronous action for demonstration purposes.
Returns:
dict: A dictionary containing the status and a generic message.
"""
message = "The 'another_action' was performed successfully!"
logger.info("Executing another_action")
return {"status": "success", "message": message}
# To add more actions:
# 1. Define your Python function here (or in another module).
# - It can be synchronous (def my_action():) or asynchronous (async def my_action():).
# - It can accept parameters, which will be supplied from `action_params` in `ui_config.yaml`.
# - It should ideally return a dictionary, often with "status" and "message" keys,
# to provide feedback to the UI (e.g., for toast notifications).
#
# 2. Register your action in an `actions_config.yaml` file.
# - This file is typically `user_config/actions_config.yaml` (to override examples)
# or `config_examples/actions_config.yaml` (for default/packaged actions).
# - Example registration:
# ```yaml
# actions:
# - id: "my_custom_action" # Unique ID for this action
# module: "visual_control_board.actions.built_in_actions" # Or your custom module path
# function: "my_newly_defined_function_name"
# ```
#
# 3. Reference the action `id` in your `ui_config.yaml` for a button:
# ```yaml
# buttons:
# - id: "my_button_for_custom_action"
# text: "Run My Custom Action"
# action_id: "my_custom_action"
# action_params: # Optional parameters for your action
# param1: "value1"
# count: 10
# ```and in API mode you can access the entire config system as read/write live updates re-rendering the interface when changes are posted back which enables (what i think are) unique features like live button additions, live icon/text changes based on external systems, animated button content (sparklines), etc:
def send_button_content_update(
button_id: str,
text: str = None,
icon_class: str = None,
style_class: str = None,
sparkline_payload: dict[str, str] | None = None,
):
"""Sends a live content update for a specific button, optionally with sparkline data."""
payload = {"button_id": button_id}
has_update = False
if text is not None:
payload["text"] = text
has_update = True
if icon_class is not None:
payload["icon_class"] = icon_class
has_update = True
if style_class is not None:
payload["style_class"] = style_class
has_update = True
if sparkline_payload is not None:
payload["sparkline"] = sparkline_payload
has_update = True
if not has_update:
# print(f"No content changes specified for button '{button_id}'. Skipping update.")
return False
try:
response = requests.post(BUTTON_UPDATE_URL, json=payload, timeout=5)
response.raise_for_status()
# print(f"Button '{button_id}' content update sent. Response: {response.json().get('message')}")
return True
except requests.exceptions.RequestException as e:
print(f"ERROR: Failed to update button '{button_id}' content.")
if hasattr(e, "response") and e.response is not None:
print(
f"Status Code: {e.response.status_code}, Response: {e.response.text[:200]}"
)
else:
print(f"Error details: {e}")
return False
def run_all_demos(
icon_text_button_id: str, sparkline_button_id: str, duration_seconds: int = 60
):
print(f"\n--- Starting All Demos (Duration: {duration_seconds}s) ---")
initialize_sparkline_data()
icon_idx = 0
start_time = time.time()
last_icon_text_update_time = 0
last_sparkline_update_time = 0
try:
while time.time() - start_time < duration_seconds:
current_loop_time = time.time()
# --- Icon/Text Demo Update (every 2 seconds) ---
if current_loop_time - last_icon_text_update_time >= 2:
time_str = time.strftime("%H:%M:%S")
random_val = random.randint(100, 999)
new_text_content = f"Icon: {random_val} @ {time_str}"
new_icon_class = ICONS_TO_CYCLE[icon_idx % len(ICONS_TO_CYCLE)]
print(
f'Updating ICON/TEXT for \'{icon_text_button_id}\': Text "{new_text_content}", Icon "{new_icon_class}"'
)
send_button_content_update(
icon_text_button_id,
text=new_text_content,
icon_class=new_icon_class,
)
icon_idx += 1
last_icon_text_update_time = current_loop_time
# --- Sparkline Demo Update (every 0.5 seconds) ---
if current_loop_time - last_sparkline_update_time >= 0.5:
update_sparkline_data_list()
sparkline_payload = {
"data": list(sparkline_data_points),
"color": SPARKLINE_BASE_COLOR,
"stroke_width": 2,
}
sparkline_text = f"Data Points: {len(sparkline_data_points)}"
# print(f"Updating SPARKLINE for '{sparkline_button_id}': Color {sparkline_payload['color']}, Points {len(sparkline_payload['data'])}")
send_button_content_update(
sparkline_button_id,
text=sparkline_text,
sparkline_payload=sparkline_payload,
)
last_sparkline_update_time = current_loop_time
time.sleep(0.1) # Main loop intervalanyway, actionboard is a useful web button board with full yaml design config and even an API driven config where you can read the system config live then splat it back for complete interface rewrites from external data sources or other inputs all triggered on-demand.
the usefulness of the actionboard isn’t really “oh wow buttons on a webpage so what” but more being dynamically reconfigurable and re-designable from a static or online config system so it can be attached to other live systems with good UI to intent to feedback full binding interfaces.
stats
actionboard is built using:
- Python, FastAPI, HTMX, Jinja2, YAML, Pydantic, WebSockets, and
uv
more details in the architecture document too.
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
CSS 1 273 218 18 37
HTML 8 160 146 7 7
JavaScript 1 242 184 23 35
Python 14 2069 1602 184 283
YAML 2 80 70 0 10
-------------------------------------------------------------------------------
Markdown 3 291 0 232 59
|- BASH 2 16 9 6 1
|- JSON 1 6 6 0 0
|- Python 1 22 16 3 3
|- YAML 1 26 24 1 1
(Total) 361 55 242 64
===============================================================================
Total 29 3115 2220 464 431
===============================================================================lively crowd