Day-to-day tools
The things you reach for every session: a performance panel that says why a sketch is slow, a data folder like Processing's, a rewind that really goes back in time, and a history of every Run with a picture of what it drew.
On this page · 6 entries
ShowStats · HideStats · DrawTime · ActualFrameRate
ShowStats()HideStats()DrawTimeActualFrameRate
The performance panel, top-left over the sketch: frames per second actually reached, how long Draw() takes (average and slowest), how much memory each frame allocates and how many garbage collections happened since it opened, with a bar graph of the last 120 frames against the frame budget. F3 over the canvas toggles it — in the IDE and in exported apps. DrawTime and ActualFrameRate read the same numbers from code; they're measured every frame whether the panel shows or not.
DataPath · SketchPath · SketchFolder · DataFolder
DataPath(name)SketchPath(name)SketchFolderDataFolder
Like Processing, a sketch's files live in a data folder next to its .cs file. Every load with a relative path — LoadImage, LoadStrings, LoadTable, LoadJSONObject, LoadFont, LoadShape, LoadSample, LoadShader2D, CreateReader... — looks in data/ first, then next to the .cs, then where the program runs; saves with a relative path (SaveFrame, SaveStrings, SaveTable...) go into the sketch folder. Exporting the sketch copies data/ next to the app, so the same paths work there. DataPath("x.csv") is the full path inside data/.
Real rewind ⏸ · ⤺
Pause with ⏸ under the canvas, drag the timeline back, and ⤺ continue from here: the sketch goes back to that exact frame — its own fields (lists, arrays, your objects), its random generator and FrameCount — and runs on from there; the future after it is forgotten. It works after ⚡ Hot Reload too, so you can go back to frame 300 and see what the code you just changed does from there (fields are matched by name).
Millis() keeps running, and the drawing state (Fill, Stroke...) stays as it is now.Code time machine Ctrl+Shift+H
Every Run keeps a copy of the code and, a moment later, a picture of what it drew. Sketch → Time machine shows them as cards — thumbnail, time, seed, lines added and removed — so you can find "the version that looked like that" by how it looked. Restore puts it back in the editor (one Ctrl+Z undoes it, and its seed is locked so it draws the same), or open it in a new tab to compare. One history per sketch file, identical runs stored once, the newest 200 kept.
Runnable example: the Game of Life, rewound
Pause with ⏸, drag back, press ⤺ and paint cells with the mouse — the story continues differently from that exact frame. F3 shows what each frame costs. Test in Dana opens it in the IDE, or paste it into a new tab and press Run. It is also in Sketch → Examples.
// Real rewind. Under the canvas: // ⏸ pause the slider scrubs through the last frames // ⤺ continue from here: the sketch's fields (the grid, the generation, // even the random generator) go back to that frame, and it runs on -- // the future after it is forgotten. // Paint cells with the mouse (drag) to make the new future different. public class MySketch : Sketch { const int Cols = 90, Rows = 60, Cell = 8; bool[,] _grid = new bool[Cols, Rows]; bool[,] _next = new bool[Cols, Rows]; int _generation; public override void Setup() { Size(Cols * Cell, Rows * Cell + 30); FrameRate(15); for (int x = 0; x < Cols; x++) for (int y = 0; y < Rows; y++) _grid[x, y] = Random(1) < 0.22f; } public override void Draw() { Step(); Background(16, 18, 26); NoStroke(); for (int x = 0; x < Cols; x++) for (int y = 0; y < Rows; y++) if (_grid[x, y]) { FillHSB((x * 2 + _generation) % 360, 55, 95); Rect(x * Cell, y * Cell, Cell - 1, Cell - 1); } Fill(255, 190); TextSize(13); Text($"generation {_generation} · ⏸ then ⤺ under the canvas to rewind for real · drag to paint", 10, Height - 10); } void Step() { for (int x = 0; x < Cols; x++) for (int y = 0; y < Rows; y++) { int n = 0; for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) if ((dx != 0 || dy != 0) && _grid[(x + dx + Cols) % Cols, (y + dy + Rows) % Rows]) n++; _next[x, y] = _grid[x, y] ? n is 2 or 3 : n == 3; } (_grid, _next) = (_next, _grid); _generation++; } public override void MouseDragged() => Paint(); public override void MousePressed() => Paint(); void Paint() { int cx = (int)(MouseX / Cell), cy = (int)(MouseY / Cell); for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) { int x = cx + dx, y = cy + dy; if (x >= 0 && x < Cols && y >= 0 && y < Rows) _grid[x, y] = true; } } }
Runnable example: why is it slow?
Raise the particles slider until the F3 graph turns red, and turn on garbage to see what new lists every frame cost. Test in Dana opens it in the IDE, or paste it into a new tab and press Run. It is also in Sketch → Examples.
// The performance panel: press F3 (here it starts open via ShowStats()). // // fps frames per second actually reached (target: FrameRate()) // Draw ms how long your Draw() takes -- the bars; the line is the budget // KB/frame memory Draw() allocates every frame // GC 0/1/2 garbage collections since the panel opened // // Raise "particles" until the bars go red. Then switch on "garbage": the same // work, but a new List every frame -- watch KB/frame and the GC counter climb. public class MySketch : Sketch { [Tweak(100, 20000)] int particles = 3000; [Tweak] bool garbage = false; private readonly List<PVector> _pos = new(); private readonly List<PVector> _vel = new(); private readonly List<PVector> _reused = new(); public override void Setup() { Size(720, 480); ShowStats(); } public override void Draw() { while (_pos.Count < particles) { _pos.Add(new PVector(Random(Width), Random(Height))); _vel.Add(PVector.Random2D().Mult(Random(0.5f, 2))); } if (_pos.Count > particles) { _pos.RemoveRange(particles, _pos.Count - particles); _vel.RemoveRange(particles, _vel.Count - particles); } // The wasteful way allocates a fresh list (and fresh vectors) every frame. var visible = garbage ? new List<PVector>() : _reused; visible.Clear(); for (int i = 0; i < _pos.Count; i++) { var p = _pos[i]; p.Add(_vel[i]); if (p.X < 0) p.X += Width; else if (p.X > Width) p.X -= Width; if (p.Y < 0) p.Y += Height; else if (p.Y > Height) p.Y -= Height; visible.Add(garbage ? p.Copy() : p); } Background(14, 16, 24); NoStroke(); Fill(120, 200, 255, 160); foreach (var p in visible) Circle(p.X, p.Y, 3); Fill(255, 180); TextSize(13); Text($"{particles} particles · garbage {(garbage ? "ON" : "off")} · F3 hides the panel", 14, Height - 14); } }