codex/mutual-exclusion-lost-update-timelines.py

mutual-exclusion-lost-update-timelines.py
"""Generate mutual-exclusion-lost-update-timelines.png in the working directory.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
The events are abstract sequentially consistent transitions, not machine code.
"""

from collections import Counter
from itertools import combinations
from pathlib import Path
import json

import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def run_schedule(schedule, protected=False):
    """Execute one schedule; an unavailable acquisition is not an enabled step."""
    pcs, registers, shared, owner, trace = [0, 0], [None, None], 0, None, []
    for thread in schedule:
        pc = pcs[thread]
        if protected:
            if pc == 0:
                if owner is not None:
                    return None
                owner = thread
                label = "acquire mutex"
            elif pc == 4:
                assert owner == thread
                owner = None
                label = "release mutex"
            else:
                assert owner == thread
                label = None
            operation = pc - 1
        else:
            label, operation = None, pc
        if label is None:
            if operation == 0:
                registers[thread] = shared
                label = f"read x: r = {shared}"
            elif operation == 1:
                registers[thread] += 1
                label = f"add 1: r = {registers[thread]}"
            elif operation == 2:
                shared = registers[thread]
                label = f"write x = {shared}"
            else:
                raise AssertionError("Invalid program counter")
        pcs[thread] += 1
        trace.append((thread, label, shared))
    assert pcs == ([5, 5] if protected else [3, 3])
    return shared, trace


def enumerate_schedules(steps, protected=False):
    outcomes = Counter()
    for positions in combinations(range(2 * steps), steps):
        selected = set(positions)
        schedule = [0 if i in selected else 1 for i in range(2 * steps)]
        result = run_schedule(schedule, protected)
        if result is not None:
            outcomes[result[0]] += 1
    return outcomes


def draw_timeline(ax, trace, title, subtitle, protected):
    colors = ["#156a9a", "#8b4b9a"]
    ax.set_xlim(-0.9, 3.5)
    ax.set_ylim(len(trace) + 0.3, -1.8)
    ax.axis("off")
    ax.set_title(title + "\n" + subtitle, fontsize=13, pad=15)
    for column, name in zip([0, 1.55, 3.05], ["T1", "T2", "shared x"]):
        ax.text(column, -0.95, name, ha="center", weight="bold", fontsize=11)
    for column in [0, 1.55]:
        ax.plot([column, column], [-0.65, len(trace) - 0.55], color="#c6ccd1", lw=1.3, zorder=0)
    if protected:
        for column, start, stop, color in [(0, 0, 4, colors[0]), (1.55, 5, 9, colors[1])]:
            ax.add_patch(FancyBboxPatch((column - 0.68, start - 0.34), 1.36, stop - start + 0.68,
                                       boxstyle="round,pad=0.04", fc=color, ec="none", alpha=0.09))
        ax.text(1.55, 2, "waiting\n(no entry)", ha="center", va="center", fontsize=10, color="#61686e")
    for i, (thread, label, shared) in enumerate(trace):
        column = 1.55 * thread
        ax.text(-0.82, i, str(i + 1), ha="center", va="center", color="#69727a", fontsize=9)
        ax.text(column, i, label, ha="center", va="center", fontsize=10, color=colors[thread],
                bbox=dict(boxstyle="round,pad=0.25", facecolor="white", edgecolor=colors[thread]))
        ax.text(3.05, i, str(shared), ha="center", va="center", fontsize=11)
    ax.text(1.15, len(trace) - 0.15, f"final x = {trace[-1][2]}", ha="center", va="top", fontsize=12,
            weight="bold", color="#28703b" if protected else "#a43232")


def main():
    unprotected = enumerate_schedules(3)
    protected = enumerate_schedules(5, True)
    assert unprotected == Counter({1: 18, 2: 2})
    assert protected == Counter({2: 2})
    bad = run_schedule([0, 1, 0, 1, 0, 1])
    good = run_schedule([0] * 5 + [1] * 5, True)
    assert bad[0] == 1 and good[0] == 2
    fig, axes = plt.subplots(1, 2, figsize=(10.6, 6.0), facecolor="white")
    draw_timeline(axes[0], bad[1], "Two increments without a mutex", "one of 18 lost-update orders (20 total)", False)
    draw_timeline(axes[1], good[1], "Protect the complete increment", "either thread may acquire first; both give x = 2", True)
    fig.text(0.5, 0.025, "Initial x = 0. Time goes downward; rows show logical steps, not measured durations.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.025, right=0.99, top=0.88, bottom=0.10, wspace=0.15)
    path = Path(__file__).stem + ".png"
    fig.savefig(path, dpi=110, facecolor="white", transparent=False)
    plt.close(fig)
    print(json.dumps({"image": path, "unprotected_orders": dict(unprotected), "mutex_orders": dict(protected)}))


if __name__ == "__main__":
    main()