Skip to content

Latency Lab v3.0.0

Choose a tag to compare

@oernster oernster released this 07 Aug 16:37
· 19 commits to main since this release

Release notes

IMPORTANT NOTE:

  • macos dmg release asset removed since I forgot to notarise it (I didn't know that step was necessary after developer signing; the latest version has been notarised).

The desktop application ships

LatencyLab has always had a desktop front end you could run from a clone. It is
now something you can install: a Windows installer, a Linux Flatpak and a macOS
disk image, each built from one entry point at the repository root, each staging
the same icons and example models beside the application.

The frozen build also starts, which it did not before. The repository-root
launcher used to rewrite the process name for tidiness. Nuitka's Qt plugin reads
that name when a Windows icon is compiled in, pulls the icons out of the file it
points at and insists on finding at least one, so pointing it at a name rather
than a file killed the application before its first window, silently, because a
release build has no console to print to. The build now smoke tests itself:
it starts the bundle headless and fails the build with the traceback if it exits.

The Linux build had a failure of the same family and it took the same amount of
finding. The Flatpak started nothing at all, because the Qt networking library
it ships is linked against a Kerberos library the freedesktop runtime does not
carry. LatencyLab never speaks to a network; the one thing that touches it is
the guard that stops you opening a second copy. That was enough to kill the
import before the first window, taking the application with it. The build now
compiles Kerberos itself, pinned by checksum and fetched on the host so the
build stays offline, for a library it will never call.

On macOS the application had an icon everywhere except the places you look for
it. The mark is drawn on a transparent canvas, which is right for the Windows
taskbar and the Linux icon set, both of which sit on something. The Dock, Finder
and the mounted disk image put it straight on the desktop instead, so what you
got was a red ring and a yellow hand floating on pale grey. macOS now gets its
own set, drawn on an opaque tile and shaped to Apple's own icon grid rather than
filling the square: inset, with the rounded corners every application beside it
in the Dock has, so LatencyLab sits at the same visual size as its neighbours
rather than looming over them. Every other platform keeps the transparent
originals; so does the badge inside the application, which is drawn on the
application's own background.

Every example is one menu item away

There is an Examples menu holding every model the application ships with, so a
fresh install has something to run before you have written a model of your own.

The set is read from disk rather than listed in code, which is the same thing
the test suite already did: dropping a model into the examples directory now
opts it into the validation test and puts it on the menu, with no second place
to remember.

Loading a model, from the menu or from the Open dialog, now flashes the Run
button's border twice. The change of state happens on the far side of the
window from where you were looking, so this points at what to do next and then
stops: two flashes, well spaced, in the same green that already means "you can
use this".

The new Checkout model is the one to start with. A storefront checkout fans
out to four back ends, two of which share a database that can only serve one at
a time; a 150ms debounce sits on the promotions branch. That debounce, added
for politeness, is the critical path in most runs. It is the shape of finding the
tool exists to produce; it appears within a second of opening the app.

The composer shows you one thing at a time

The Model Composer was a panel down the right-hand side holding every part of a
model at once: the system settings, the contexts, every task and the wiring, in
one column you scrolled. With the checkout model open that column ran to nearly
five thousand pixels, the tasks alone accounting for three and a half thousand
of it, so anything past the second task was found by scrolling and then
remembering where it was.

It is now a window of its own, opening at nearly the size of the application.
The left side lists what the model is made of, a row per section and a row per
task; the right side shows whichever one you pick. Every part now fits without
scrolling at all.

The list keeps up with the model as you type. Renaming a task renames its row as
you go rather than rebuilding the list under your cursor, adding one takes you
straight to it; a task you have not named yet still holds its place rather
than vanishing from the list until it earns a name.

Some things went with the side panel. Compose no longer competes with the
results panel for the right-hand side, so opening one is no longer a question
about the other and the results are still there when you close the composer. The
Compose button is no longer a toggle, because a window covering the application
does not need a button underneath it to say so. The prompt about unexported
results stayed, since that was never about the layout.

A model you opened is a model you can edit

The composer could only ever build a model from nothing. It can now be filled
from the model that is already loaded, so opening a file and changing it is one
action rather than a retyping exercise: everything the file says, including
delays and per-task categories, comes back into the editors.

Compose and Edit are both on the new Model menu and both in the tray, as drawn
icons with tooltips rather than one wide captioned button. Edit is inert until
something is loaded and says so.

An editor that is open follows the next model you open, rather than carrying on
showing the previous one. It leaves a composer alone if you were typing a model
into it from scratch, because replacing that would be losing your work rather
than refreshing a view.

The Contexts table shows the contexts it holds. It used to be given a height
that had never looked at them, so it reserved empty space for a model with two
and cut off a model with ten; it collapsed to about two rows whenever the
panel was short. Names were clipped inside a fixed column width while the room
they needed sat empty in the same row. The table is now as tall as its contents,
up to the point where it would start pushing the rest of the panel out of reach;
the name column takes whatever width is going.

The rows fit what stands in them, too. A concurrency box is taller than the row
it was being drawn into, so its lower half was simply not painted: the down
arrow was missing entirely, you could count a context up but never back down;
the number sat against the bottom edge of what was left rather than in the
middle of its box. Those were one fault. A row is now as tall as the tallest
thing in it.

Every field is the same size as every other field

The controls in the Model Composer were three different heights. A concurrency
box stood at 51 pixels, a distribution parameter at 19 and a text field at 22,
which is why the editors read as cramped in some places and roomy in others.

The cause was a single word. The rule that gives an input its size named one
kind of spin box; the toolkit treats the whole-number and decimal versions
as unrelated cousins rather than as parent and child, so the rule reached one
and silently missed the other. Text fields were not named at all and had been
running on the toolkit's own defaults throughout. Every kind of field is now
covered by one rule; a test asserts that nothing anywhere is drawn smaller
than it asks for, in either theme, rather than checking today's numbers.

Two things had been quietly overruling that rule as well: an explicit minimum
set on the dropdown lists, which let a panel squeeze one to half its proper
size; a sizing hint on the composer's scrolling panel that read as "no
taller than it needs" and meant "may be made shorter than it needs". That last
one was leaving nineteen controls drawn smaller than their own minimum, which is
what squashed the parameter arrows.

The toolbar is a tray

The icons used to sit directly on the window with nothing marking where the
chrome stopped and the content began. They now sit in a band of their own, with
its own surface and a bottom edge.

The application mark in the middle of the band is now the button that shows the
distributions; it turns banana yellow while they are up. It used to be
decoration, sitting there doing nothing while the control that opened the panel
was one more small glyph in the row on the left. The mark is the most prominent
thing on the band and the panel is the point of having run anything, so they are
one thing now rather than two competing from opposite ends.

It is a toggle rather than a one-way opener, so the press that shows the panel
is the press that puts it away; it follows the panel rather than the click:
closing the panel by its own cross leaves the mark telling the truth. The
emoji it replaces is gone with it, which is one fewer thing on the band drawn by
whichever font the platform happened to pick.

The mark's hands are banana rather than white, which is easier to pick out and
is the colour the rest of the application already uses to mean something. That
is one drawing, so it changes everywhere at once: the application, the taskbar,
the installer, the shortcut, the About box and the website all read from the
same file. While it is switched on the mark is redrawn in dark ink, because
banana hands on a banana button would be no hands at all.

One LatencyLab at a time

Launching the application again no longer opens a second copy. The one already
running comes to the front instead.

This also fixes the taskbar. The installer registers a shortcut carrying an
application identity that the application itself never claimed, so Windows
treated the pinned icon and the running window as two unrelated things:
clicking the icon did nothing at all; the jump list opened another copy
beside the first. The application now claims the same identity the installer
registers.

The guard is a socket rather than a lock, because a lock would have stopped the
second copy and left you looking at nothing, which is worse than the duplicate
window it prevents. The second copy asks the first to come forward and exits.

Open and Export start where your files are

Both dialogs used to open in the working directory. For an installed
application that is the install location, so the file picker opened inside the
program itself. They now start in your Downloads folder; an export arrives
pre-named.

Cancel cancels

Cancel used to stop the user interface paying attention. The simulation carried
on to the end and its results were thrown away, so a hundred thousand runs
started by accident could not be stopped.

It now stops the work. The simulator is CPU-bound and cannot safely be
interrupted from outside, so it is asked to stop at the boundary between one run
and the next: the worst case is one run's delay, which is a number you can be
told; no run is ever left half simulated. A cancelled run reports how many
completed and refuses to aggregate them, because percentiles over a truncated
set look exactly like real ones while describing a system nobody asked about.

The keyboard reaches everything

Keyboard navigation is now one explicit ring rather than whatever tab order the
toolkit inferred. Tab and Right step forward, Shift+Tab and Left step back; the ring wraps at both ends. The horizontal arrows work everywhere, including
inside the menu bar and the charts, both of which used to swallow them.

The side panels were the worst of it, being siblings of the main layout rather
than children, so the traversal never saw them: not one of the Model Composer's
thirteen controls could be reached from the keyboard. That is fixed; the
composer has since become a dialog, which is a window of its own and reached the
way every other dialog is.

The window also starts neutral. Nothing is focused and no menu drops open until
you press something. Dialogs deliberately do the opposite and open already
focused on their first useful control, because a dialog was opened on purpose.

Every inert control says why it is inert

Run, Cancel, Export and Distributions all open unusable on a cold start, which
was correct and unexplained. A disabled control now wears a red ring and carries
a tooltip saying what would make it usable; the keyboard ring skips it
rather than stopping on something that does nothing.

Run is gated on a loaded model, so the old "No model" dialog is gone: it
interrupted you to say what the button should have been saying all along.

Both themes come from one description

Light and dark used to be two stylesheets kept in step by hand. They are now
generated from one template and one set of named colours per theme, so the same
rules hold in both by construction: no ring at rest, a green ring when a control
is usable and under the pointer or the keyboard, a permanent red ring when it is
not usable. The brand colour never draws a ring, so it can go on meaning
something.

The theme is one button rather than two; it names the theme it will switch
to rather than the one already in force. The sun needed an explicit request for
emoji presentation to come out yellow rather than as a white glyph, which is why
one of the pair used to look wrong.

The accent is banana yellow throughout: the application, the installer, the
website and the mark itself. The teal it replaces was doing the loudest job in
the interface, sitting as a filled block on the button that says the composer is
open.

Scrolling past a control no longer changes it; no longer takes the keyboard
with it. Reading down the Model Composer with the wheel used to walk through
every context's concurrency and every task's distribution on the way, silently
rewriting the model you were only trying to look at. The two halves of that were
one fault: the toolkit hands these controls the keyboard as soon as the wheel
passes over them; a control holding the keyboard is exactly the one whose
wheel is meant for it. Scrolling past something is no longer a way of choosing
it, so the value stays put, the keyboard stays where you left it; the panel
scrolls instead.

The panel also stops getting stuck part way down. A control sitting inside a
table sits inside two scrolling surfaces; the inner one was quietly taking
the wheel meant for the outer, so the panel stopped moving over some rows and
not others. The wheel now goes to the first surface with somewhere to go.

Menus, tooltips and dropdown lists are now painted on a surface of their own,
lighter than the window in the dark theme and outlined in the strongest edge
colour in the set. They used to be painted in the window colour with no border
at all, which meant an open menu was not merely low contrast: it had no visible
edge; its entries read as text lying on the window behind it. The
separation is asserted against rendered pixels rather than against the
stylesheet, so it cannot quietly come back.

There is a Guide now

A new button on the band, an open book immediately left of the information one,
opens a guide to using LatencyLab. It is drawn in two colours, white covers with
banana ruled lines and a banana page edge, so it reads as a manual with
something in it rather than as another blank document. It starts with six numbered steps that take
you from a cold start to a finished run and a distribution on screen, with no
explanation attached to any of them, because somebody opening a guide usually
wants the application to do something in the next minute.

Everything after that is reasons: what you are looking at once a run finishes,
how to change one thing and compare, how to write a model from nothing through
the composer's four parts; a section on why you would choose each setting
rather than the one next to it. Why more runs buy you a tail you can quote, why
the seed is worth holding still, what a concurrency of one really means, when a
lognormal is honest and a normal is not; why a politeness delay is so often
the thing that owns the median.

It is separate from How to Read on purpose. That one is about interpreting
output; this one is about working the application. They answer questions asked
at different moments.

Like the licences, it reads itself: leave it open and it descends slowly, holds
at the end, rewinds and goes again; it hands control straight back the
moment you touch it.

The application is blue; the mark stands off it

Every filled control was lavender purple. It is now a mildly dark royal blue,
one token moved, so buttons, the checked states and the site that describes them
all followed together.

The mark had a harder problem than a preference. Its case was the same purple as
the button it is drawn on, so the centrepiece of the toolbar was a watermark:
measured, twenty-seven levels of luminance apart at the top of its gradient and
zero at the bottom, against the twenty this project treats as the least a person
can see. Recolouring the button alone would have kept the fault, so the case is
now a warm amber instead. Warm against a cool fill rather than a lighter shade
of the same colour, which is what stops the two ever quietly rejoining the next
time the fill moves. The hands stay banana and the stopwatch stays a stopwatch.

The two remaining single-tone icons went with it. Compose and Edit were plain
white line drawings and read as watermarks for the same reason, so they now
split the way the Guide's book already did: the part that merely sits there
takes the button's ink, the part that says what the button DOES takes the
accent. The graph's nodes and its plus, the pencil on the document. Disabled
still mutes both tones, because a glyph that keeps its accent while the rest of
it greys out reads as half-available, which is not a state this application has.

Long text reads itself

Licence and guidance text descends slowly, holds at the end, rewinds and
repeats. Touch it and it stops where you are, then picks up from there rather
than from the top.

It is attached only to surfaces that scroll by pixels. The plain text widgets
scroll by lines, where the same gentle drift becomes a line jumping at a time
and the rewind becomes fifteen lines a tick: measured over the same three
hundred lines, that is 4348 units of travel against 293.

About says what the application is built on

The About dialog carries the real application icon rather than an emoji painted
onto a blank square and credits every dependency the application actually
ships with its licence. LatencyLab is distributed under the GPL and the LGPL and
both oblige it to say what it is built on, so the credit list is pinned against
the project's own requirements by a test: a list that drifts from the real
dependency set is worse than none.

One version string, everywhere

The repository used to carry three version numbers that disagreed with each
other: one in the core package, one in the UI package and one in the packaging
metadata. Anything asking LatencyLab what version it was got a different answer
depending on which one it happened to reach.

There is now a single VERSION file at the project root and nothing else holds
a version string. The core reads it at runtime, the UI reports the core's
number rather than inventing its own, the packaging metadata reads the same
file and the website is stamped from it by a small script that the build runs
for you. The About dialog, a built package and the site footer can no longer
drift apart.

The licence now says what the code actually is

The packaging metadata declared MIT while the repository shipped GPL-3.0 code,
which meant a published package advertised terms that did not match its
contents. That is fixed: the metadata now declares GPL-3.0 and points at the
licence text it actually ships.

The split is stated plainly in the README and in the architecture notes. The
simulation core is GPL-3.0 and the PySide6 desktop front end is LGPL-3.0, which
is exactly what the running application already showed under its Help menu.

The published package contains what the documentation promised

Every document said the distributable was the command line core and that the
desktop UI was something you run from a clone. The packaging rule did not agree:
a pattern intended to pick up the core also matched the UI package, so the UI
was being swept into the build. The UI is now excluded by name and a test
asserts what a built package contains, rather than the documentation describing
it and nobody checking.

The coverage gate is a gate again

The 100% coverage threshold used to apply only when somebody remembered to type
the long form of the test command. It now lives in the project configuration,
so a plain pytest run enforces it and there is no way to run the suite
without it. Formatting and linting run as assertions inside the suite for the
same reason.

LatencyLab has a website

There is a GitHub Pages site for the project, covering who the tool is for and
who it is deliberately not for, what it actually does, how to get started and a
look at the desktop UI against a real model run one hundred thousand times.

A second page sets out the reasoning behind the tool at length: why latency
arguments run on intuition until the decisions have already hardened, plus what
moving the question to design time buys you. Site addresses point at
ernster.dev, which is where the rest of the projects live.

Two more pages carry what the application itself tells you, cut down to what is
worth reading before you have installed anything. One is the Guide: the six
steps, what the three panels mean, how to change one thing and run it again, the
four parts of a model and a line on each setting. The other is the information
panel on reading the output, reduced from nine sections of prose to six points
and a short note on how to use them. The showcase now also shows the composer
itself, both writing a model from nothing and changing one that is loaded.

Documentation and housekeeping

The project keeps a standing technical-debt reference: what is still open, what
is deliberately left alone and what only looks like debt. Everything it listed
as open has now been done, so it records only the standing decisions that keep
it that way.

The architecture notes were rewritten against the code rather than around it.
They now cover the cancellation boundary, the keyboard ring, the theme model,
the auto-scroller and the whole delivery path; every reference in them
points at the line it claims to.

Dead code went with it: a panel kept alive by a comment about backward
compatibility, an installer helper kept alive only by its own test; two
defensive catches guarding against a window losing a dock it owns.