Watched
restarts, the watch that starts the piece again, together with @Saved state and a checkpoint cadence. Press c to crash the piece and h to hang it, and it comes back with one ring per run. Quit it and run it again, and the record carries on
- Run
swift run --package-path Examples Example-Installation-Watched- Source
- Examples/Installation/Watched/Sketch.swift
import Ollin
/// A piece that remembers how far it got, gets itself back up, and keeps the
/// record of every time it had to.
///
/// A run on a wall ends in ways a run at a desk does not: a crash at three in
/// the morning, or a frame that never finishes and leaves the screen frozen
/// with the process still alive. `restarts` puts a small watch over the run. It
/// starts the piece again after a crash, and after a piece that is still
/// running stops answering, which is how a frozen frame is found at all.
///
/// Each launch draws its own ring, from the outside in, and each ring is as long
/// as that run lasted. So the picture is a record of the piece's own life: a
/// short arc is a run that ended badly, and the ring still growing is this one.
///
/// That record is something the clock alone cannot rebuild: the same run at
/// the same second could not tell you how the earlier ones ended. So the runs
/// are marked `@Saved`, and the checkpoint writes them down every five seconds.
/// It covers both kinds of end. Quit with Command-Q, an ordinary end that
/// nothing restarts, then run it again: the rings are where you left them.
/// Crash it, and the watch brings it back with the older rings intact. Run it
/// with `--fresh` to start over, and the saved record is left alone for next
/// time. The file is JSON you can open, in
/// `~/Library/Application Support/Ollin/`.
///
/// Try it. Press **c** to crash it and **h** to hang it, and watch the piece
/// come back on its own with the last ring stopped where it stopped. Without
/// the checkpoint beside `restarts` the piece would come back at the beginning
/// every time.
///
/// Five seconds between checkpoints and ten without answering are
/// demonstration settings; a minute, and the half-minute stall default, suit a
/// piece that is actually going up somewhere. Run it with `--no-installation`
/// to work on it in an ordinary window, where a crash stays crashed. See
/// `Docs/Output/Installation.md`.
@main
final class Watched: Sketch {
override var installation: Installation {
Installation(checkpoint: .every(seconds: 5),
restarts: .onFailure(stalledAfter: 10))
}
/// How long each launch so far lasted, oldest first. Saved, so the record
/// survives the crash it is a record of.
@Saved var runs: [Double] = []
/// This run's own length, which is what the last entry above is kept at.
private var thisRun = 0.0
private var counted = false
private let ink = Ramp([Color(red: 0.20, green: 0.26, blue: 0.38),
Color(red: 0.36, green: 0.62, blue: 0.80),
Color(red: 0.94, green: 0.82, blue: 0.54),
Color(red: 0.88, green: 0.42, blue: 0.38)])
override func setup() {
textFont(OutlineFont.system)
}
override func draw() {
// The count goes up on the first frame rather than in `setup()`, and
// that is not a style choice: saved state is put back *after* `setup()`
// runs, because `setup()` is where a sketch fills its properties in. A
// launch counted there would be overwritten a moment later.
if !counted {
counted = true
runs.append(0)
}
thisRun += deltaTime
runs[runs.count - 1] = thisRun
background(Color(white: 0.05))
noFill()
strokeCap(.round)
let shown = Array(runs.suffix(12))
let outer = shortSide * 0.40
let step = outer / 15
for (index, seconds) in shown.enumerated() {
let radius = outer - Double(index) * step
let live = index == shown.count - 1
// Two minutes is a whole turn, so a long run reads as a full ring
// and a run that ended after a few seconds reads as a stub.
let turn = min(1, seconds / 120)
stroke(ink.color(at: live ? 0.75 : Double(index) / Double(max(1, shown.count)))
.withAlpha(live ? 1 : 0.55))
strokeWeight(live ? step * 0.42 : step * 0.30)
drawArc(center: center, radiusX: radius, radiusY: radius,
start: -.pi / 2, stop: -.pi / 2 + turn * .tau)
}
label()
}
private func label() {
noStroke()
textAlign(.center, .middle)
fill(Color(white: 0.92))
textSize(shortSide * 0.045)
drawText("run \(runs.count)", center.x, center.y - shortSide * 0.02)
textSize(shortSide * 0.028)
fill(Color(white: 0.55))
drawText("\(Int(thisRun))s of \(Int(runs.reduce(0, +)))s in all",
center.x, center.y + shortSide * 0.03)
textSize(shortSide * 0.022)
fill(Color(white: 0.38))
drawText("c crashes it, h hangs it", center.x, height * 0.93)
}
override func keyPressed() {
switch key?.lowercased() ?? "" {
case "c":
// The kind of end nobody is there to see. The watch reads the exit
// status, starts another run, and the ring above stops here.
fatalError("crashing on purpose, to show the piece coming back")
case "h":
// The other kind, and the one an exit status cannot show: the
// process stays perfectly alive with the main thread stuck in this
// frame for ever. Only the heartbeat that stops arriving finds it.
while true {}
default:
break
}
}
}