Appendix A · The DSL, in full
What this holds
| Purpose | Every node kind, every field, every distribution, and what the build checks |
| Source | sizing/dsl.py, sizing/expr.py, sizing/evaluate.py |
A model is a YAML file. It declares named quantities, each with a unit, and how they depend on each other. There is no evaluation order in the file, no cells, no hidden state, and no way to write a number in one place and have it mean something else in another.
The whole language is on this page. It is small on purpose: a model somebody has to learn a system to read is a model nobody reads.
The file
model: web_service
title: A web service and its data, on a fleet of Linux hosts
currency: USD
description: >-
The book's running example: a service that takes requests and keeps records, run on some
number of ordinary hosts, sized from a stated workload and costed over five years. Requests
arrive and data accumulates; a host has cores, memory and disk, and each of those is a chain
that ends in a count of hosts. One of the three binds.
It is a sizing model. Its ceilings are the queueing knee, the point where more hosts stop
helping, the working set outgrowing memory, disk filling, and a host dying at the busy hour;
its one measured constant is how well the stored records compress. Its one rate constant —
CPU time per request — belongs on a rig nobody has declared, and is held as a labelled claim
until one is.
Vendor-neutral by construction. Nothing below names a product; every figure is a workload
assumption, a price you would get quoted, a spec sheet, or the measured constant.
Four keys before the nodes begin. model is the identifier, and it is the directory name.
title is the name a reader sees, and it defaults to the identifier when a model does not give
one. currency is declared rather than assumed, so that a model mixing two of them fails to
typecheck instead of quietly adding them. description is prose, and it is where a model says what it is
for — the one thing a reader cannot reconstruct from the graph.
The four node kinds
| Node kind | Count | What it carries |
|---|---|---|
input | 26 | a value or a distribution, a provenance kind and a source |
derived | 23 | a formula, whose declared unit is checked against what it produces |
measured | 5 | a stamped result, a standard error, and the implementation it belongs to |
ceiling | 4 | a limit, a declared headroom, and a reason |
| classified as a sizing model — it has measured constants or ceilings in it, so sampling the inputs is not sufficient on its own |
Source — observability-reference · every input on a slider · 2 constant(s) not yet measured
The last row is the distinction the book is built on, and the file decides it rather than its
author’s opinion of it. A model containing a measured node or a ceiling node is a sizing
model. It has an empirical constant that belongs to one stack at one version, or a limit past
which its arithmetic stops describing anything. In either case sampling the inputs is not
sufficient on its own. A model with neither is a cost model.
Here are the kinds, as the loader defines them:
class Node:
name: str
unit: str
label: str | None = None
note: str | None = None
kind: ClassVar[str] = "node"
def depends_on(self) -> set[str]:
return set()
@property
def display(self) -> str:
"""What a figure calls this node. Falls back to the name, which is already readable."""
return self.label or self.name.replace("_", " ")
#: Who settles an input. A model cannot work this out for itself: the nearest signal it has is
#: whether the input was given a shape, and ch02 is about how poor a proxy that is -- nothing in
#: that chapter's file has a shape, so the growth rate reads as a choice. Declaring it makes the
#: split a claim the model makes rather than an inference from how finished the file is.
#:
#: ``you`` is a choice somebody made and can change: the fleet, the horizon, a headroom margin.
#: ``world`` is an observation, whether or not it has been given a shape yet: the busy hour, the
#: records held, a price. ``definition`` is an identity nobody chooses and the world does not
#: vary -- a year in seconds, one host, one request.
DECIDED_BY = ("you", "world", "definition")
@dataclass(frozen=True)
class Input(Node):
value: float | None = None
distribution: dict | None = None
provenance: Provenance | None = None
slider: tuple[float, float] | None = None
#: One of :data:`DECIDED_BY`. Empty only in a file that has not declared it, which
#: ``verify-models.py`` refuses.
decided: str = ""
kind: ClassVar[str] = "input"
@property
def is_uncertain(self) -> bool:
return self.distribution is not None
@property
def is_yours(self) -> bool:
"""Whether a reader could have chosen this differently."""
return self.decided == "you"
@dataclass(frozen=True)
class Derived(Node):
formula: dict = field(default_factory=dict)
formula_text: str = ""
kind: ClassVar[str] = "derived"
def depends_on(self) -> set[str]:
return expr.refs(self.formula)
@dataclass(frozen=True)
class Measured(Node):
result: str = ""
#: The stamped payload, or None when nobody has taken this measurement yet.
measurement: dict | None = None
kind: ClassVar[str] = "measured"
@property
def is_measured(self) -> bool:
return self.measurement is not None
@property
def value(self) -> float | None:
if self.measurement is None:
return None
return float(self.measurement["summary"]["value"])
@property
def sd(self) -> float:
"""The measurement's standard error, or zero if it was reported without one.
Zero is a claim, and a loud one: it says this constant was measured exactly. Almost
nothing is, so ``scripts/verify-models.py`` reports a measured node with no stated
uncertainty rather than letting it pass as precision.
"""
if self.measurement is None:
return 0.0
return float(self.measurement["summary"].get("sd", 0.0))
@property
def stack(self) -> str | None:
"""What was measured — the implementation and version this constant belongs to.
The reason a measured constant is not a fact about the world. ch03: change the encoder,
change the version, change the shape of your data, and this number is about something
else.
"""
if self.measurement is None:
return None
return self.measurement.get("produced_by", {}).get("stack")
@dataclass(frozen=True)
class Ceiling(Node):
of: dict = field(default_factory=dict)
of_text: str = ""
limit: dict = field(default_factory=dict)
limit_text: str = ""
headroom: dict = field(default_factory=dict)
headroom_text: str = ""
because: str = ""
kind: ClassVar[str] = "ceiling"
def depends_on(self) -> set[str]:
"""Everything the ceiling reads — including its limit and its margin.
``limit`` and ``headroom`` are expressions rather than numbers so that a margin used by a
sizing formula and the margin a ceiling checks against can be *the same node*. Writing
0.25 in both places is how a model comes to be sized for one headroom and audited against
another, six months after anybody remembers there were two.
"""
return expr.refs(self.of) | expr.refs(self.limit) | expr.refs(self.headroom)
@property
def declares_headroom(self) -> bool:
"""Whether a margin was stated at all. A ceiling without one is not a sizing rule."""
return bool(self.headroom_text.strip())
input — a number somebody chose
A value, or a distribution, or both. Plus a provenance kind, and a source the build will not let you leave empty:
annual_growth:
kind: input
decided: world
unit: dimensionless
label: annual growth factor
distribution: {lognormal: {p10: 1.12, p90: 1.60}}
provenance:
kind: assumption
source: "ch04 — lognormal because growth compounds and cannot be negative. The p10/p90
say: surprised below 12% a year, surprised above 60%. One factor for requests and for
records, because users drive both."
range: [1.0, 2.0]
range is what the interactive page turns into a slider. It is a plausible span for a reader to
explore, not a claim about the distribution — the distribution is the claim about the
distribution.
derived — arithmetic
A formula over other nodes, and a unit that has to agree with what the formula produces:
peak_request_rate:
kind: derived
unit: request/second
label: peak request rate at horizon
formula: peak_request_rate_t0 * annual_growth ** horizon_periods
The formula language is deliberately not Python. It is parsed with Python’s own parser and then walked, and only these functions survive the walk:
FUNCTIONS: dict[str, Any] = {
"min": min,
"max": max,
"ceil": math.ceil,
"floor": math.floor,
"sqrt": math.sqrt,
"log": math.log,
"exp": math.exp,
}
Arithmetic is + - * / ** and unary minus. There is no name lookup other than other nodes, no
attribute access, no calls to anything not in that dictionary. A model file cannot do anything, so
running a stranger’s model is reading their arithmetic rather than executing their code.
measured — a constant somebody measured
Not a value. A reference to a stamped result:
record_compression:
kind: measured
unit: dimensionless
label: record compression ratio
result: records-compression
The number, its standard error and the implementation it belongs to all come from
bench/results/. That is what separates a measured constant from an input that happens to have
been measured once. If the result does not exist, the node has no value and neither does anything
downstream of it — the state propagates by itself and the figures say not yet measured rather
than showing an estimate (ch03).
ceiling — where the arithmetic stops working
A quantity, a limit, a margin, and a reason:
queueing_headroom:
kind: ceiling
unit: dimensionless
label: utilisation at the busy hour
of: utilisation
limit: 1
headroom: queueing_margin
because: >-
Not a capacity limit: a fleet at ninety per cent has not run out of anything, it has a
queue in front of it. The margin is large because the cost of crossing it is paid in
latency by every request, not in a failure somebody gets paged for (ch06).
of, limit and headroom are all expressions rather than numbers, so that a margin a sizing
formula uses and a margin a ceiling audits against can be the same node. Writing the same figure
in both places is how a design comes to be sized for one headroom and checked against another,
some months after anybody remembers there were two.
because is required and headroom is required. A ceiling with a limit and no margin is not a
sizing rule but a comparison, and the build refuses it
(ch11).
Provenance
#: How much a modeller is claiming when they write a number down.
#:
#: Three, and the gap between the first two is the one that costs money. A ``fact`` is something
#: this repository can point at: a stamped measurement, an invoice, a published specification. A
#: ``vendor_claim`` is a number somebody selling you something has stated — often true, never
#: checked here, and coloured differently in every figure so that it cannot quietly become the
#: first kind. An ``assumption`` is a decision, and naming it as one is what lets a reviewer
#: argue with it.
PROVENANCE_KINDS = ("fact", "vendor_claim", "assumption")
Three kinds, and a source string on every input. A vendor’s claim is not a fact, and that gap is the one that costs money. Every figure in the book colours the three differently, and none of them is ever silently promoted.
Distributions
An input may declare exactly one shape. These are the four, and adding a fifth is a percentile function and a line:
SHAPES: dict[str, Callable[..., np.ndarray]] = {
"uniform": uniform_ppf,
"triangular": triangular_ppf,
"lognormal": lognormal_ppf,
"normal": normal_ppf_scaled,
}
Appendix C is one page each on what they assume and how they lie.
Outputs
Which nodes are answers. Everything else in the graph is working:
outputs:
# The sizing and the price, first: what a reader of ch12 and ch18 wants.
- hosts_recommended
- hosts
- tco
- cost_per_million_requests
- cost_per_stored_tb_month
- capex
- annual_opex
- annual_energy
# The ceilings, and what each one is about.
- queueing_headroom
- failure_headroom
- cache_fill
- disk_fill
- scaling_loss
- coordination_headroom
# Part II's own figures. Terminal in the finished model, and outputs because a quantity the
# book quotes has to be one -- a node that feeds nothing and is reported nowhere is a leftover.
- utilisation
- residence_time
- waiting_time
- concurrency
- in_flight_unqueued
- optimism
- headroom_to_peak
The list decides what the tables report and what the tornado is drawn against. It is not a restriction — every node keeps its value and its distribution, and the interactive page will show you any of them. It is an editorial judgement about which handful of numbers somebody is going to be asked about.
Correlations
A model may declare that two inputs move together, with a reason:
correlations:
- a: peak_request_rate_t0
b: service_demand
rho: 0.4
because: >-
The unhappy one. A busier service is usually a slower one per request — caches miss more,
locks are held longer, the garbage collector runs while somebody is waiting. Treating the
two as independent understates the tail of every figure in this model, and it understates
it exactly when the fleet is busiest.
- a: host_price
b: network_price_per_host
rho: 0.5
because: >-
Both are quoted by the same supply chain in the same quarter. A year when hosts are scarce
is usually a year when optics are, and treating them as independent narrows the interval
on capital cost by pretending one can save you from the other.
The reason is not optional. A coefficient with no reason attached is a number somebody will copy into the next model without knowing what it was for (ch14).
Scenarios
A scenario is a small file beside the model. It overrides inputs, names why, and pins the sample count and the seed so the run can be reproduced exactly:
scenario: sized_for_growth
title: Sized for the growth we might get, not the growth we expect
because: >-
The reference scenario buys what the point estimates recommend, and the model then puts a
good share of its futures over the knee and past the working set's margin. This scenario buys
the fleet the p90 growth case needs instead, and is here so the two can be read side by side:
what the extra capital buys is a number, the fall in every P(over) is a number, and the
decision between them is a judgement somebody has to make and defend (ch21).
samples: 100000
seed: 20260916
overrides:
hosts: 131
Scenarios are how you compare two designs without editing either one into the other (ch21).
What the build checks
scripts/verify-models.py runs on every model on every push, and fails on any of:
A unit that does not follow. Every formula is evaluated in units as well as in numbers. A declared unit that disagrees with what the formula produces is an error. One that agrees dimensionally but differs by a factor, dollars per TB per year against per month, is recorded as a conversion and applied, not waved through (Appendix D).
An input with no provenance, or a
factthat cites nothing.An input that is sampled and does not name its shape in its source. The distribution is a claim about what can happen, and it is the claim to argue with first (Appendix C).
A ceiling with no headroom, or no reason.
A measured node whose result reports no uncertainty, which is a claim to have measured something exactly.
A cycle, or a node that depends on something that does not exist.
A measured constant with no ceiling anywhere in the model. A model with empirical inputs and no declared limit is claiming that nothing in it changes regime. That may be true; say it by declaring the ceiling, not by leaving one out. The classification comes from the file and cannot be asserted by hand.
A node that feeds no output, which is either a leftover or an output somebody forgot to declare. Also a model that declares no outputs at all, since nothing in it can then be checked.
A scenario overriding a node that does not exist, or a derived one. Overriding a derived node would be editing the arithmetic while claiming to change an assumption.
The graph
The running example as ch02 leaves it, the smallest graph in the book: five inputs, three derived nodes and nothing else. It has no ceiling and no measured constant, so it is a cost model, and ch06 is where it stops being one. Appendix F is the model with all four kinds in it. Reading right to left from any answer gives exactly the quantities it rests on; reading left to right shows how few inputs most of the graph is downstream of.
Running it
python3 -m bench.run_models # evaluate, sample and stamp every model
python3 -m bench.run_models --model NAME # just one
python3 scripts/verify-models.py # units, provenance, ceilings, classification
python3 scripts/build-viewers.py # the interactive pages