Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Render with: d2 --theme 0 --elk-nodeNodeBetweenLayers 45 how-it-works.d2 how-it-works.svg
vars: {
d2-config: {
layout-engine: elk
pad: 30
}
}

grid-columns: 1
grid-gap: 48

classes: {
panel: {style: {fill: "#FFFFFF"; stroke: "#B0BEC5"; border-radius: 12}}
heading: {shape: text; style: {font-size: 28; bold: true}}
pair: {style: {fill: transparent; stroke: transparent}}
user: {shape: person; width: 60; height: 60; style: {font-size: 18}}
lock: {shape: hexagon; width: 170; style: {fill: "#FDECEA"; stroke: "#C62828"; font-size: 17; bold: true}}
slow: {width: 110; style: {fill: "#FFF3E0"; stroke: "#E65100"; border-radius: 6; font-size: 16}}
run: {width: 110; style: {fill: "#E8F4EA"; stroke: "#2E7D32"; border-radius: 6; font-size: 16}}
step: {width: 190; style: {fill: "#E3F2FD"; stroke: "#1565C0"; border-radius: 6; font-size: 16}}
row: {style: {fill: "#F5F9FF"; stroke: "#90CAF9"; border-radius: 10; font-size: 18; bold: true}}
wait: {style: {stroke: "#C62828"; stroke-width: 2; stroke-dash: 4; font-size: 15; font-color: "#C62828"}}
flow: {style: {stroke: "#1565C0"; stroke-width: 2; font-size: 15}}
next: {style: {stroke: "#1565C0"; stroke-width: 2; stroke-dash: 4; font-size: 15}}
seq: {style: {stroke: "#E65100"; stroke-width: 2}}
note: {shape: text; style: {font-size: 17}}
tasks: {label.near: bottom-left; style: {fill: "#F7FCF8"; stroke: "#81C784"; stroke-dash: 3; border-radius: 8; font-size: 15; font-color: "#2E7D32"; bold: true}}
}

before: "" {
class: pair
grid-columns: 1
grid-gap: 14

title: "Shiny 1.8: everyone takes turns behind one lock" {class: heading}

panel: "" {
class: panel
direction: right

carol: Carol {class: user}
bob: Bob {class: user}
alice: Alice {class: user}

lock: "One lock for\nthe whole app" {class: lock}

line: "Global queue" {
label.near: bottom-right
style: {fill: "#FFFBF5"; stroke: "#FFCC80"; border-radius: 10; font-size: 16; font-color: "#E65100"; bold: true}
direction: right
a1: "Alice\nSales" {class: slow}
a2: "Alice\nForecast" {class: slow}
b1: "Bob\nSales" {class: slow}
b2: "Bob\nForecast" {class: slow}
c1: "Carol\nSales" {class: slow}
c2: "Carol\nForecast" {class: slow}
a1 -> a2 -> b1 -> b2 -> c1 -> c2 {class: seq}
}

carol -> lock: "waits" {class: wait}
bob -> lock: "waits" {class: wait}
alice -> lock: "has the lock" {class: flow}
lock -> line.a1 {class: flow}
}
}

after: "" {
class: pair
grid-columns: 1
grid-gap: 14

title: "Shiny 1.9: each session runs on its own, at the same time" {class: heading}

panel: "" {
class: panel
grid-columns: 4
horizontal-gap: 49


alice: "Alice's session" {
class: row
direction: down
who: Alice {class: user}
tasks: "Per-session tasks" {
class: tasks
s: Sales {class: run}
f: Forecast {class: run}
}
out: "Send both outputs\ntogether" {class: step}
nxt: "Apply Alice's\nnext input" {class: step}
who -> tasks.s {class: flow}
who -> tasks.f {class: flow}
tasks.s -> out {class: flow}
tasks.f -> out {class: flow}
out -> nxt: "then" {class: next}
}

bob: "Bob's session" {
class: row
direction: down
who: Bob {class: user}
tasks: "Per-session tasks" {
class: tasks
s: Sales {class: run}
f: Forecast {class: run}
}
out: "Send both outputs\ntogether" {class: step}
nxt: "Apply Bob's\nnext input" {class: step}
who -> tasks.s {class: flow}
who -> tasks.f {class: flow}
tasks.s -> out {class: flow}
tasks.f -> out {class: flow}
out -> nxt: "then" {class: next}
}

carol: "Carol's session" {
class: row
direction: down
who: Carol {class: user}
tasks: "Per-session tasks" {
class: tasks
s: Sales {class: run}
f: Forecast {class: run}
}
out: "Send both outputs\ntogether" {class: step}
nxt: "Apply Carol's\nnext input" {class: step}
who -> tasks.s {class: flow}
who -> tasks.f {class: flow}
tasks.s -> out {class: flow}
tasks.f -> out {class: flow}
out -> nxt: "then" {class: next}
}

note: |md
**All six outputs run at the same time.**<br/>
While one output waits for its query,<br/>
the others keep going, in every session.

**Each session keeps its own order.**<br/>
A session sends its outputs together,<br/>
and applies the next input only after<br/>
that, so inputs never change halfway<br/>
through.
| {
shape: text
style: {font-size: 18}
}
}
}
85 changes: 85 additions & 0 deletions content/blog/shiny-python-concurrent-async/_feature-src/capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Regenerate the post's feature images from feature-anim.html.

- feature.png (static fallback / social preview): the animation's final
frame, captured at 1.6x device scale so the output is exactly 1920x1080
- feature.gif (animated, used as the post's `image`): frame-by-frame capture
driven by the page's deterministic `seek(t)` function

Usage (requires ffmpeg on PATH):

uv run --with playwright python capture.py

Outputs are written to the post directory (the parent of this folder).
"""

import pathlib
import subprocess
import tempfile

from playwright.sync_api import sync_playwright

HERE = pathlib.Path(__file__).parent
POST_DIR = HERE.parent
SRC = (HERE / "feature-anim.html").as_uri()

WIDTH, HEIGHT = 1200, 675 # 16:9
LOOP_MS = 5600
FPS = 10
N_FRAMES = LOOP_MS * FPS // 1000


def await_fonts(page):
"""Block until the self-hosted site fonts are loaded, and fail loudly if
they didn't load -- a silent fallback to a system sans would look wrong."""
page.evaluate("() => document.fonts.ready")
for spec in ('600 16px "Open Sans"', '500 16px "Source Code Pro"'):
if not page.evaluate(f"() => document.fonts.check('{spec}')"):
raise SystemExit(f"font not loaded: {spec}")


with sync_playwright() as p:
browser = p.chromium.launch()

# Static poster -> feature.png at 1920x1080 (final resting frame)
page = browser.new_page(
viewport={"width": WIDTH, "height": HEIGHT},
device_scale_factor=1.6,
)
page.goto(SRC)
await_fonts(page)
page.evaluate(f"seek({LOOP_MS - 100})")
page.wait_for_timeout(300)
page.screenshot(path=str(POST_DIR / "feature.png"))
page.close()
print("feature.png written (1920x1080)")

# Animated card -> feature.gif at 1200x675
page = browser.new_page(viewport={"width": WIDTH, "height": HEIGHT})
page.goto(SRC)
await_fonts(page)
page.wait_for_timeout(300)

with tempfile.TemporaryDirectory() as tmp:
frames = pathlib.Path(tmp)
for i in range(N_FRAMES):
page.evaluate(f"seek({i * 1000 / FPS})")
page.screenshot(path=str(frames / f"frame_{i:03d}.png"))
print(f"frame {i + 1}/{N_FRAMES}", end="\r")
browser.close()

print("\nassembling gif...")
subprocess.run(
[
"ffmpeg", "-y",
"-framerate", str(FPS),
"-i", str(frames / "frame_%03d.png"),
"-vf", "split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=4",
"-loop", "0",
str(POST_DIR / "feature.gif"),
],
check=True,
capture_output=True,
)

size = (POST_DIR / "feature.gif").stat().st_size
print(f"feature.gif: {size / 1024:.0f} KB")
Loading
Loading