· 7 min read
Performance, Safety and Security Constraints in SysML v2
How fast, how safe, how protected — none of it comes out of a behavioral model, and it shouldn't. Here is the vocabulary for writing those constraints so they are worth having.
A behavioral model tells you what happens, in what order, across which interfaces, and what each step must produce. It tells you nothing about how fast, how safe, or how protected — and it should not. A use case is an existential statement about interaction. “The response arrives within two seconds” is a universal claim over every run, and nothing in the behavior says it.
So you write those by hand. This is the vocabulary for writing them in a form that a test can actually settle.
A constraint is a Boolean expression. That is the whole of it.
Everything here reduces to one idea. A constraint body is a Boolean expression, and nothing else:
require constraint retrievalIsFastEnough {
evidence.retrieveModelContextDuration <= 2 [s]
}
That is the whole construct. A name for what the claim means, and an expression that is either true or false of a given run.
It is worth being blunt about the thing that trips people, because it looks
reasonable and is not: a constraint body is not a place to bind values.
Writing in budget = 2 [s]; inside one is an assignment where a predicate
belongs, and a validator will reject it. If a number needs a name, give it one
as an attribute on the requirement and refer to it:
requirement <'REQ-P-001'> retrieveContextLatency {
subject evidence : TimedEvidence;
attribute stepBudget :> duration = 2 [s];
require constraint {
evidence.retrieveModelContextDuration <= stepBudget
}
}
Now the budget appears in a table, changes in one place when a trade moves it, and the constraint still reads as the inequality it is.
Assume, require, assert — three different claims
All three take a constraint. They differ in what they claim, and choosing wrong produces a model that looks rigorous and proves the wrong thing.
The standard library states the relationship between the first two outright.
RequirementConstraintCheck in Requirements.sysml defines
the separate assumptions and required constraints such that, if all the assumptions are true, then all the required constraints must be true
with assumptions documented as “assumptions that must hold for the required
constraints to apply.”
So a requirement is an implication. The assumptions say when the obligation is in force; the required constraints say what must then hold.
| What it claims | If it turns out false | |
|---|---|---|
assume | a condition under which the requirement applies | the requirement does not apply — the run is not applicable, not failed |
require | what must hold whenever it does apply | the requirement is violated |
assert | that something is simply true, unconditionally | the model is inconsistent |
Worth being precise about one thing, because it is easy to reach for the wrong shorthand: an assumption is not “somebody else’s problem.” It can be about the subject and still be an assumption.
assume constraint { pump.inletPressure >= 2 [bar] }
require constraint { pump.flowRate >= 80 [L/min] }
Both clauses are about the pump. The first is still an assumption, because it says when the flow obligation is in force — below two bar the requirement is not violated, it simply does not apply. Ownership is a separate question from logical role, and only the second one picks the keyword.
assert sits outside that implication entirely. It claims something holds full
stop. Every one of the 38 assert constraint usages in the shipped Systems and
Domain libraries is an invariant — tensor arithmetic, dimensional consistency,
the identity element of a sum — a fact true by construction of the thing being
defined. A false assert does not mean the system misbehaved; it means the
model contradicts itself.
assume is the one people skip, and it is the one that makes a performance
requirement honest. “Retrieve the context within two seconds” is not
achievable if the repository sits on the far side of a congested link. Either
that link budget is an assumption you are stating, or it is an obligation you
are accepting:
requirement def RetrieveContextLatency {
subject evidence : TimedEvidence;
doc /* The application shall retrieve the model context within 2 s of
* being asked, given a repository that answers within 1.2 s. */
assume constraint repositoryRespondsInTime {
evidence.repositoryRoundTripDuration <= 1.2 [s]
}
require constraint retrievalIsFastEnough {
evidence.retrieveModelContextDuration <= 2 [s]
}
}
Without the assume, a test run over a slow network reports a violated
requirement. With it, that same run reports an unmet precondition — a
different fact, and the one an engineer actually needs. One sends somebody to
optimize code that was never the problem. The other sends them to the network.
Two places a constraint can live
This is the distinction that decides whether a performance constraint is testable or merely stated.
Design-time, over parts and their attributes. Answers does this architecture close? The values are allocations and budgets, checked by inspection before anything is built. This is what a trade study moves.
Verification-time, over the evidence record of one observed run. Answers did this run meet it? The values are measurements, checked by a test.
The same inequality serves both. Only what it is written over changes:
// design time — written over allocations and budgets
require constraint { retrieveAllocation + generateAllocation
+ validateAllocation <= endToEndBudget }
// verification time — written over the evidence of one run
require constraint { evidence.retrieveModelContextDuration <= 2 [s] }
Both are require. Nothing about design time makes a constraint an assert —
an allocation that does not close is a design that does not work, which is
exactly what a requirement is for. assert is for what is true by construction,
and a budget split is a judgment somebody made.
A constraint written only at design time is an unverified claim. A constraint written only at verification time has no budget behind it and cannot be allocated. Real programs need both — and the second’s budget should be the first’s allocation, not a number typed twice.
One budget, allocated
A latency budget is not a number you assert once. It is a quantity you divide, and the division is the trade.
Because the left-hand side is an expression, one inequality handles a decomposition of any width — two allocations or twelve, the same line:
requirement def EndToEndLatency {
attribute endToEndBudget :> duration = 5 [s];
attribute retrieveAllocation :> duration = 2 [s];
attribute generateAllocation :> duration = 2 [s];
attribute validateAllocation :> duration = 1 [s];
require constraint allocationsClose {
retrieveAllocation + generateAllocation + validateAllocation
<= endToEndBudget
}
}
Each allocation then becomes a requirement on a part, satisfied by the part that owns it — so every component has a number traceable to the one budget above it. Change an allocation and the requirement tells you whether the split still closes.
Where the measured values come from
The derived evidence record has no durations, because the behavior has none. Add them by specializing the derived definition rather than editing it — a derived package regenerates byte-identically, and hand edits are lost:
item def TimedEvidence :> GenerateAndValidateEvidenceDef {
attribute retrieveModelContextDuration :> duration;
attribute repositoryRoundTripDuration :> duration;
attribute heartbeatInterval :> duration;
attribute awaitAcknowledgementElapsed :> duration;
attribute awaitAcknowledgementTimedOut : Boolean;
}
Every new attribute is a probe — something the application has to publish before any of these constraints can be checked. That is the same currency as the instrumentation ledger a derivation produces, and it belongs in the same tally. A performance requirement is not free just because it was easy to type.
Worth keeping the records separable, too. A program that cares about latency and not about audit should not be asked to build the audit probes.
Performance patterns
A step completes within a budget. The common case, and the one to get right first. Put the budget in a requirement attribute rather than burying a literal in the expression, so the number has a name, appears in a table, and changes in one place when a trade moves it:
requirement <'REQ-P-002'> generateModelLatency {
subject evidence : TimedEvidence;
attribute stepBudget :> duration = 4 [s];
require constraint { evidence.generateModelDuration <= stepBudget }
}
A periodic emission stays near its nominal period. Reach for a tolerance, not a ceiling:
require constraint {
evidence.heartbeatInterval >= 0.95 [s] and
evidence.heartbeatInterval <= 1.05 [s]
}
A ceiling would not catch a heartbeat arriving twice as fast — which, for a watchdog, is exactly as wrong as one arriving late.
A timeout fires when it should, and only then. This one needs a biconditional, and the reason is worth sitting with:
require constraint {
evidence.awaitAcknowledgementTimedOut ==
(evidence.awaitAcknowledgementElapsed >= 30 [s])
}
timedOut implies elapsed >= timeout would be satisfied by a system that never
times out at all. A requirement that cannot fail is not a requirement.
Safety and security
The mechanics are identical. What changes is that most of these are implications rather than equalities: if the bad thing is possible, the mitigation is present — and it has to stay true when the bad thing is absent.
A hazardous action is interlocked:
require constraint {
evidence.committed implies evidence.consistencyAnalysisPassed
}
Compare that with a derived prohibition. A derivation can produce “shall not
commit when the analysis failed” only where the use case models that branch and
the shape of the flow licenses the claim — an else that cannot hold with its
sibling, a step nothing else can reach. This constraint is stronger and does not
depend on the branch being modeled at all: it forbids the action under every
unlisted circumstance too. That extra strength is exactly why a person has to
assert it rather than a tool inferring it.
The safe state is reached in time:
requirement <'REQ-S-002'> failSafeTiming {
subject evidence : SafetyEvidence;
attribute ftti :> duration = 0.2 [s];
require constraint {
evidence.inconsistenciesFound implies
evidence.timeToRecordRejection <= ftti
}
}
The fault-tolerant time interval comes from a hazard analysis. Nothing in a behavioral model produces it, and a tool that invented one would be doing something much worse than leaving it out.
Failures are annunciated. This one is free — both values already exist:
require constraint {
(evidence.stateAfter == StateKind::useCaseFailed)
implies evidence.notifyAnalystPerformed
}
Note the left of the implication is a comparison, not a flag. Nothing in the
model has to carry a failureOccurred Boolean for this to work — “the run ended
in the failed state” is already expressible from the evidence you have, and a
constraint body takes any Boolean expression.
What this buys you
None of this is derivable. Every number here — the budget, the tolerance, the FTTI, the allocation split — is a judgment somebody made, and no amount of modeling sophistication will produce it from behavior.
What the vocabulary buys is that once those judgments exist, they stop being prose in a specification and become expressions a test can evaluate. The budget has a name. The allocation is traceable to the part that owns it. The assumption is written down, so a slow-network run reports the right failure. And every probe the constraints need is counted alongside the ones the behavior needed, rather than discovered late.
The hard part was always the judgment. What this seems to help with is stopping it evaporate somewhere between the hazard analysis and the test report.
More of this, on video
Walkthroughs in SysML v2 and MBSE tooling, with the model on screen.
