{"id":493404,"date":"2026-09-03T17:30:04","date_gmt":"2026-09-03T17:30:04","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=493404"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=493404","title":{"rendered":"I Kept Average Test Time at 10 Seconds but Changed Only Its Variance: Why Unpredictable Tests Break Developer Focus"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Two test suites can have exactly the same average runtime and still create completely different development workflows. I modeled 300 code \u2192 test \u2192 fix cycles while keeping mean feedback latency close to ten seconds and changing only its variance. One runner behaved predictably. The other was usually fast but occasionally became extremely slow. The average stayed almost the same. The cost of waiting did not.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b37\/5bc\/b7e\/b375bcb7efbd71a5857edbefc46dbd0c.png\" width=\"1672\" height=\"941\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/b37\/5bc\/b7e\/b375bcb7efbd71a5857edbefc46dbd0c.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b37\/5bc\/b7e\/b375bcb7efbd71a5857edbefc46dbd0c.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>Introduction<\/h3>\n<p>A test that always takes ten seconds is annoying, but after a few runs it becomes predictable. I press the shortcut, know roughly how much time I have, and usually stay inside the same task.<\/p>\n<p>A test that sometimes finishes in two seconds and sometimes takes thirty is different. Even if its average duration is also ten seconds, I cannot build the same expectation around it. After three seconds I do not know whether the result is about to arrive or whether I have another half minute to kill.<\/p>\n<p>That uncertainty changes the decision to wait.<\/p>\n<p>Most discussions about build and test performance focus on average execution time. Sometimes p95 and p99 appear in CI dashboards, but even then they are usually treated as infrastructure metrics. I wanted to look at the same distribution from another angle: what does latency variance do to the development loop itself?<\/p>\n<p>So I built a simple simulation of 300 code \u2192 test \u2192 fix cycles.<\/p>\n<p>The goal was not to estimate a universal human attention threshold. The model is intentionally synthetic. I wanted to isolate one variable that is usually mixed together with everything else: predictability.<\/p>\n<p>Both simulated test runners have approximately the same average runtime.<\/p>\n<p>One is boring.<\/p>\n<p>The other has a long tail.<\/p>\n<p>That difference turns out to matter much more than the average suggests.<\/p>\n<h3>1. Same Average, Different Systems<\/h3>\n<p>The first runner is almost deterministic. Most executions finish near ten seconds.<\/p>\n<p>A typical sequence might look like this:<\/p>\n<pre><code>9.410.29.810.110.49.710.09.9<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The second runner is usually much faster, but occasionally enters a slow path:<\/p>\n<pre><code>2.13.02.74.22.431.82.930.9<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The averages can be made almost identical.<\/p>\n<p>If I reduce both systems to one metric, they look equivalent.<\/p>\n<p>That is exactly the problem.<\/p>\n<p>The stable runner has a narrow distribution. Its median, p95 and p99 remain relatively close.<\/p>\n<p>The unstable runner can have a much better median while producing an ugly tail.<\/p>\n<p>That means a developer can experience the second system as fast most of the time and still trust it much less.<\/p>\n<p>This is not only about comfort.<\/p>\n<p>Interactive development is a control loop:<\/p>\n<pre><code>change code\u2193run test\u2193receive result\u2193update hypothesis\u2193make next change<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The usefulness of the loop depends not only on how quickly feedback arrives on average, but also on whether I can predict when it will arrive.<\/p>\n<p>A predictable delay lets me stay attached to the current hypothesis.<\/p>\n<p>An unpredictable delay creates a decision point.<\/p>\n<p>Do I keep waiting?<\/p>\n<p>Do I open documentation?<\/p>\n<p>Do I answer a message?<\/p>\n<p>Do I start another small task?<\/p>\n<p>Once that decision appears, test runtime stops being the full cost of feedback.<\/p>\n<h3>2. The Important Metric Is Remaining Wait, Not Average Wait<\/h3>\n<p>Suppose a test has already been running for eight seconds.<\/p>\n<p>Knowing that average runtime is ten seconds is not enough.<\/p>\n<p>What I actually care about is:<\/p>\n<pre><code>How much longer is this particular run likely to take,given that it has already lasted eight seconds?<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Mathematically, this is the expected residual lifetime:<\/p>\n<pre><code>E[T - t | T &gt; t]<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>T is total execution time.<\/p>\n<p>t is elapsed waiting time.<\/p>\n<p>This quantity behaves very differently for narrow and heavy-tailed distributions.<\/p>\n<p>For a nearly deterministic ten-second process, waiting longer usually means I am getting closer to completion.<\/p>\n<p>If the test has already survived nine seconds, the expected remaining wait is tiny.<\/p>\n<p>That makes waiting rational.<\/p>\n<p>Now consider a runner where most tests finish in three seconds but some take thirty.<\/p>\n<p>If the test is still running after eight seconds, I have learned something important.<\/p>\n<p>This is probably not one of the fast runs.<\/p>\n<p>The fact that I have already waited a long time does not necessarily mean completion is near.<\/p>\n<p>It may mean I have entered the slow population.<\/p>\n<p>That changes the economics of waiting completely.<\/p>\n<p>The same effect can be described through the survival function:<\/p>\n<pre><code>S(t) = P(T &gt; t)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This is the probability that the process is still running after time t.<\/p>\n<p>From it, we can derive the hazard function:<\/p>\n<pre><code>h(t) = f(t) \/ S(t)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>where f(t) is the probability density of completion time.<\/p>\n<p>The name sounds more dramatic than the idea.<\/p>\n<p>The hazard rate tells me how likely completion is around the current moment, assuming the test has not finished yet.<\/p>\n<p>For a predictable runner, the hazard becomes high near the usual completion point.<\/p>\n<p>For a long-tailed runner, surviving beyond the normal fast region may actually be bad news.<\/p>\n<p>That is the first reason average latency is a weak metric for interactive tooling.<\/p>\n<p>Two systems can share the same mean and create opposite incentives to wait.<\/p>\n<h3>3. Modeling 300 Feedback Cycles<\/h3>\n<p>I split the simulation into two groups of 300 runs.<\/p>\n<p>The first distribution stays tightly clustered around ten seconds.<\/p>\n<p>The second is a mixture.<\/p>\n<p>Most runs are fast, but a smaller group enters a much slower path.<\/p>\n<p>Conceptually:<\/p>\n<pre><code>Stable runnermean \u2248 10 slow variancesmall distance between p50 and p99<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>and:<\/p>\n<pre><code>Unstable runnermean \u2248 10 shigh variancefast medianlarge p95very large p99<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This is the important part of the experiment.<\/p>\n<p>I am not changing average runtime deliberately.<\/p>\n<p>I am changing uncertainty.<\/p>\n<p>For the stable runner, I can approximate execution time with something close to a narrow normal distribution centered near ten seconds.<\/p>\n<p>For the unstable runner, a mixture model works better:<\/p>\n<pre><code>T =    fast distribution, with high probability    slow distribution, with low probability<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>For example:<\/p>\n<pre><code>P(fast) = 0.8P(slow) = 0.2<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The exact coefficients are not the conclusion.<\/p>\n<p>They are knobs.<\/p>\n<p>What matters is that the second distribution has the same approximate mean but a much longer right tail.<\/p>\n<p>Once I compare them through p50, p95 and p99 rather than mean alone, the difference becomes obvious.<\/p>\n<p>The stable runner is mediocre every time.<\/p>\n<p>The unstable runner is excellent most of the time and terrible just often enough to destroy confidence.<\/p>\n<p>That trade-off appears constantly in real systems.<\/p>\n<p>A build step usually finishes quickly, except when dependency resolution behaves differently.<\/p>\n<p>An integration test normally takes a few seconds, except when a container cold-starts.<\/p>\n<p>A local database query is fast until something invalidates a cache.<\/p>\n<p>A compiler is predictable until incremental state disappears.<\/p>\n<p>From the perspective of the average, these events are noise.<\/p>\n<p>From the perspective of the developer, they are remembered.<\/p>\n<h3>4. Developer Behavior Has Memory<\/h3>\n<p>A simple latency model assumes every test run is independent.<\/p>\n<p>I do not think developer behavior works that way.<\/p>\n<p>Suppose five recent runs took:<\/p>\n<pre><code>91011109<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Now compare that with:<\/p>\n<pre><code>2182189<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The average is similar.<\/p>\n<p>My expectation is not.<\/p>\n<p>After the first sequence, I trust the tool.<\/p>\n<p>After the second, I know that another long delay can appear without warning.<\/p>\n<p>That memory affects the next run.<\/p>\n<p>Even if the sixth test completes in three seconds, I may leave after two because the previous history has trained me not to wait.<\/p>\n<p>Now a fast test can produce a slow feedback loop.<\/p>\n<p>The test itself did nothing wrong.<\/p>\n<p>Its predecessors changed my behavior.<\/p>\n<p>A simple way to represent recent expectation is an exponentially weighted estimate:<\/p>\n<pre><code>E_n = \u03b1T_n-1 + (1 - \u03b1)E_n-1<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Recent runs receive more weight than old ones.<\/p>\n<p>But expected duration alone is still not enough.<\/p>\n<p>The developer also learns variance.<\/p>\n<p>A more useful internal state would contain at least:<\/p>\n<pre><code>expected durationuncertaintyrecent extreme events<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This matters because outliers can have a delayed cost.<\/p>\n<p>A thirty-second test does not necessarily waste only thirty seconds.<\/p>\n<p>It can make me switch earlier during the next several runs.<\/p>\n<p>The latency penalty leaks forward.<\/p>\n<p>That is difficult to see in CI dashboards because test systems measure executions independently.<\/p>\n<p>Humans do not experience them independently.<\/p>\n<h3>5. Adding Context Switching to the Model<\/h3>\n<p>I then added a simple decision rule.<\/p>\n<p>At each point during a wait, the simulated developer can remain in the task or leave the current context.<\/p>\n<p>The switching probability depends on three variables:<\/p>\n<pre><code>elapsed timeexpected remaining timeuncertainty<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Conceptually:<\/p>\n<pre><code>P(switch) =    sigmoid(        a        + b1 \u00d7 elapsed        + b2 \u00d7 expected_remaining        + b3 \u00d7 uncertainty    )<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The coefficients are not empirical claims.<\/p>\n<p>They only provide a mechanism for comparing distributions.<\/p>\n<p>The result I care about is qualitative.<\/p>\n<p>With stable tests, uncertainty remains low. Even when execution is not especially fast, waiting is relatively easy to justify.<\/p>\n<p>With unstable tests, a few long runs increase the perceived uncertainty of future runs.<\/p>\n<p>Switching probability rises earlier.<\/p>\n<p>This creates situations like:<\/p>\n<pre><code>run 1   3 srun 2   4 srun 3   32 srun 4   3 srun 5   4 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Run 4 is technically fast.<\/p>\n<p>But after run 3, I may leave the IDE after two seconds.<\/p>\n<p>Run 4 completes one second later.<\/p>\n<p>I do not notice immediately because I am already somewhere else.<\/p>\n<p>Then I return.<\/p>\n<p>Then I reconstruct the original code context.<\/p>\n<p>A three-second test has just produced a feedback loop lasting much longer than three seconds.<\/p>\n<p>The test duration is not the problem.<\/p>\n<p>Trust in the timing model is.<\/p>\n<h3>6. Effective Feedback Latency Is Larger Than Test Latency<\/h3>\n<p>This suggests separating two metrics.<\/p>\n<p>The first is obvious:<\/p>\n<pre><code>T_test<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The second is what I actually experience:<\/p>\n<pre><code>T_effective<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>A simple decomposition is:<\/p>\n<pre><code>T_effective =    T_test    + T_detection    + T_reconstruction<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>T_detection is the delay between test completion and me noticing it.<\/p>\n<p>T_reconstruction is the time required to resume meaningful work on the original problem.<\/p>\n<p>If I stay inside the IDE, both values may be close to zero.<\/p>\n<p>If I switch context, both can become substantial.<\/p>\n<p>Now variance becomes expensive in a second way.<\/p>\n<p>A long-tailed runner creates more context switches.<\/p>\n<p>More context switches increase detection delay.<\/p>\n<p>More detection delay increases effective feedback latency.<\/p>\n<p>Higher effective latency makes future waiting less attractive.<\/p>\n<p>That creates a feedback loop:<\/p>\n<pre><code>unpredictable latency\u2193more context switching\u2193more missed completions\u2193higher effective latency\u2193less trust in waiting\u2193more context switching<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The tool develops a reputation.<\/p>\n<p>Once that reputation becomes bad, even fast runs stop receiving the benefit of patience.<\/p>\n<p>This is why I think performance consistency can matter more than average performance for interactive tooling.<\/p>\n<h3>7. Slow Outliers Also Change How Code Is Written<\/h3>\n<p>There is another effect that has nothing to do with returning to the IDE.<\/p>\n<p>Slow feedback changes batch size.<\/p>\n<p>When tests are predictable and cheap, this workflow feels natural:<\/p>\n<pre><code>changetestchangetestchangetest<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>When test duration becomes unpredictable, I start wanting to amortize the risk.<\/p>\n<p>The workflow changes:<\/p>\n<pre><code>changechangechangechangetest<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>That reduces the number of waits.<\/p>\n<p>It also increases the number of possible causes when a test fails.<\/p>\n<p>With one change between passing and failing states, the search space is small.<\/p>\n<p>With four changes, it is larger.<\/p>\n<p>Now the test may not have become slower at all.<\/p>\n<p>The development process around it became harder.<\/p>\n<p>This produces another loop:<\/p>\n<pre><code>unpredictable tests\u2193less frequent runs\u2193larger edit batches\u2193larger failure search space\u2193more expensive debugging\u2193less willingness to run tests frequently<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This is where a performance problem starts changing engineering behavior.<\/p>\n<p>At that point test latency is no longer just infrastructure latency.<\/p>\n<p>It affects the granularity of development.<\/p>\n<h3>8. Why Removing the Worst 5 Percent May Be More Valuable Than Improving the Median<\/h3>\n<p>Imagine two optimization projects.<\/p>\n<p>The first improves the common case:<\/p>\n<pre><code>beforep50 = 8 sp95 = 10 sp99 = 12 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>After optimization:<\/p>\n<pre><code>p50 = 5 sp95 = 9 sp99 = 12 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The median becomes clearly better.<\/p>\n<p>Now consider a different system:<\/p>\n<pre><code>p50 = 8 sp95 = 14 sp99 = 35 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Suppose the second optimization barely changes the median but removes the tail:<\/p>\n<pre><code>p50 = 8 sp95 = 10 sp99 = 12 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Which optimization improves development more?<\/p>\n<p>If I only count average seconds saved, the first one may look better.<\/p>\n<p>If I care about predictability, the second may have a larger behavioral effect.<\/p>\n<p>It removes the pathological runs that teach developers not to trust the tool.<\/p>\n<p>This is similar to tail-latency engineering in distributed systems.<\/p>\n<p>A tiny percentage of extremely slow requests can consume resources, hold queues and create cascading effects far beyond their frequency.<\/p>\n<p>In developer tooling, part of the cascade happens inside the human side of the loop.<\/p>\n<p>The outlier interrupts work.<\/p>\n<p>The interruption changes future waiting behavior.<\/p>\n<p>That behavior changes how often tests are run.<\/p>\n<p>The new test frequency changes the size of code batches.<\/p>\n<p>One outlier can therefore affect several later cycles.<\/p>\n<h3>9. What I Would Measure in a Real Development Environment<\/h3>\n<p>A real experiment does not require recording code, keystrokes, browser history or message contents.<\/p>\n<p>Timing metadata is enough.<\/p>\n<p>From the test runner I would collect:<\/p>\n<pre><code>run startedrun completedtest targetresult<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>From the IDE or operating system:<\/p>\n<pre><code>IDE lost focusIDE regained focusfirst editor actionnext test launch<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Then a single cycle could look like this:<\/p>\n<pre><code>12:00:00.000   test started12:00:04.200   IDE lost focus12:00:08.600   test completed12:00:14.100   IDE regained focus12:00:16.400   first meaningful action<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Raw test latency:<\/p>\n<pre><code>8.6 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Effective feedback latency:<\/p>\n<pre><code>16.4 s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The test runner sees 8.6 seconds.<\/p>\n<p>The developer experiences more than sixteen.<\/p>\n<p>Across hundreds of runs, I would compare not only duration but also:<\/p>\n<ul>\n<li>\n<p> p50 <\/p>\n<\/li>\n<li>\n<p> p95 <\/p>\n<\/li>\n<li>\n<p> p99 <\/p>\n<\/li>\n<li>\n<p> coefficient of variation <\/p>\n<\/li>\n<li>\n<p> extreme outlier frequency <\/p>\n<\/li>\n<li>\n<p> probability of leaving the IDE <\/p>\n<\/li>\n<li>\n<p> completion-to-return delay <\/p>\n<\/li>\n<li>\n<p> return-to-first-action delay <\/p>\n<\/li>\n<li>\n<p> edit batch size between test runs <\/p>\n<\/li>\n<\/ul>\n<p>That last metric may be especially useful.<\/p>\n<p>If latency variance really changes development strategy, the number of edits between test runs should increase as timing becomes less predictable.<\/p>\n<p>That gives a behavioral signal that can be measured without interpreting subjective focus.<\/p>\n<h3>10. Average Runtime May Be the Wrong Optimization Target<\/h3>\n<p>Performance teams naturally want a single number.<\/p>\n<p>Average test duration is convenient.<\/p>\n<p>Median duration is better.<\/p>\n<p>p95 and p99 reveal more.<\/p>\n<p>For interactive development, I think even that may be incomplete.<\/p>\n<p>The metric I actually care about is closer to:<\/p>\n<pre><code>probability that feedback arrivesbefore the developer abandons the current context<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>That quantity combines the tool and the user.<\/p>\n<p>It depends on runtime distribution, recent history, task complexity and context-switch cost.<\/p>\n<p>This also means two test suites with the same p50 and p99 could still behave differently if one has a more predictable structure between those points.<\/p>\n<p>A narrow distribution creates a reliable expectation.<\/p>\n<p>A multimodal distribution can create uncertainty even when common percentile values look acceptable.<\/p>\n<p>For example, a test runner can have two distinct modes:<\/p>\n<pre><code>fast modeslow mode<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>That often happens for concrete technical reasons:<\/p>\n<pre><code>warm cache vs cold cacheincremental build vs full rebuildrunning container vs container startuplocal dependency vs remote dependencyJIT warm state vs cold state<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>If those modes are not visible to the developer, the tool feels random.<\/p>\n<p>Sometimes the best performance improvement may therefore be exposing the mode rather than reducing runtime.<\/p>\n<p>If the IDE tells me that a full rebuild is happening and will probably take thirty seconds, uncertainty falls even though latency does not.<\/p>\n<p>Predictability can be improved through information as well as speed.<\/p>\n<p>That is an interesting design implication.<\/p>\n<h3>Conclusion<\/h3>\n<p>Two development tools can have exactly the same average latency and still create completely different workflows.<\/p>\n<p>A stable ten-second test is slow but understandable.<\/p>\n<p>A test that averages ten seconds by alternating between two and thirty seconds creates uncertainty.<\/p>\n<p>That uncertainty matters because waiting is a prediction problem.<\/p>\n<p>The developer is continuously estimating whether staying inside the current task is still rational.<\/p>\n<p>A predictable tool makes that estimate easy.<\/p>\n<p>An unpredictable one teaches the developer to leave.<\/p>\n<p>Once that happens, the cost spreads beyond the slow run itself. Context switches create detection delay and reconstruction cost. A pathological run changes expectations for later runs. Testing becomes less frequent. Edit batches become larger. Failed tests become harder to localize.<\/p>\n<p>This is why I no longer think average test runtime is enough to describe developer feedback quality.<\/p>\n<p>The distribution matters.<\/p>\n<p>Its tail matters.<\/p>\n<p>Its recent history matters.<\/p>\n<p>And its predictability may matter as much as its speed.<\/p>\n<p>If I had to choose between making every test slightly faster and removing the handful of pathological runs that make the system unpredictable, I would no longer automatically choose the first option.<\/p>\n<p>The worst test may not be the one that takes the longest.<\/p>\n<p>It may be the one that makes me stop trusting all the tests that come after it.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1078302\/\">https:\/\/habr.com\/ru\/articles\/1078302\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Two test suites can have exactly the same average runtime and still create completely different development workflows. I modeled 300 code \u2192 test \u2192 fix cycles while keeping mean feedback latency close to ten seconds and changing only its variance. One runner behaved predictably. The other was usually fast but occasionally became extremely slow. The average stayed almost the same. The cost of waiting did not.IntroductionA test that always takes ten seconds is annoying, but after a few runs it becomes predictable. I press the shortcut, know roughly how much time I have, and usually stay inside the same task.A test that sometimes finishes in two seconds and sometimes takes thirty is different. Even if its average duration is also ten seconds, I cannot build the same expectation around it. After three seconds I do not know whether the result is about to arrive or whether I have another half minute to kill.That uncertainty changes the decision to wait.Most discussions about build and test performance focus on average execution time. Sometimes p95 and p99 appear in CI dashboards, but even then they are usually treated as infrastructure metrics. I wanted to look at the same distribution from another angle: what does latency variance do to the development loop itself?So I built a simple simulation of 300 code \u2192 test \u2192 fix cycles.The goal was not to estimate a universal human attention threshold. The model is intentionally synthetic. I wanted to isolate one variable that is usually mixed together with everything else: predictability.Both simulated test runners have approximately the same average runtime.One is boring.The other has a long tail.That difference turns out to matter much more than the average suggests.1. Same Average, Different SystemsThe first runner is almost deterministic. Most executions finish near ten seconds.A typical sequence might look like this:9.410.29.810.110.49.710.09.9The second runner is usually much faster, but occasionally enters a slow path:2.13.02.74.22.431.82.930.9The averages can be made almost identical.If I reduce both systems to one metric, they look equivalent.That is exactly the problem.The stable runner has a narrow distribution. Its median, p95 and p99 remain relatively close.The unstable runner can have a much better median while producing an ugly tail.That means a developer can experience the second system as fast most of the time and still trust it much less.This is not only about comfort.Interactive development is a control loop:change code\u2193run test\u2193receive result\u2193update hypothesis\u2193make next changeThe usefulness of the loop depends not only on how quickly feedback arrives on average, but also on whether I can predict when it will arrive.A predictable delay lets me stay attached to the current hypothesis.An unpredictable delay creates a decision point.Do I keep waiting?Do I open documentation?Do I answer a message?Do I start another small task?Once that decision appears, test runtime stops being the full cost of feedback.2. The Important Metric Is Remaining Wait, Not Average WaitSuppose a test has already been running for eight seconds.Knowing that average runtime is ten seconds is not enough.What I actually care about is:How much longer is this particular run likely to take,given that it has already lasted eight seconds?Mathematically, this is the expected residual lifetime:E[T &#8212; t | T &gt; t]T is total execution time.t is elapsed waiting time.This quantity behaves very differently for narrow and heavy-tailed distributions.For a nearly deterministic ten-second process, waiting longer usually means I am getting closer to completion.If the test has already survived nine seconds, the expected remaining wait is tiny.That makes waiting rational.Now consider a runner where most tests finish in three seconds but some take thirty.If the test is still running after eight seconds, I have learned something important.This is probably not one of the fast runs.The fact that I have already waited a long time does not necessarily mean completion is near.It may mean I have entered the slow population.That changes the economics of waiting completely.The same effect can be described through the survival function:S(t) = P(T &gt; t)This is the probability that the process is still running after time t.From it, we can derive the hazard function:h(t) = f(t) \/ S(t)where f(t) is the probability density of completion time.The name sounds more dramatic than the idea.The hazard rate tells me how likely completion is around the current moment, assuming the test has not finished yet.For a predictable runner, the hazard becomes high near the usual completion point.For a long-tailed runner, surviving beyond the normal fast region may actually be bad news.That is the first reason average latency is a weak metric for interactive tooling.Two systems can share the same mean and create opposite incentives to wait.3. Modeling 300 Feedback CyclesI split the simulation into two groups of 300 runs.The first distribution stays tightly clustered around ten seconds.The second is a mixture.Most runs are fast, but a smaller group enters a much slower path.Conceptually:Stable runnermean \u2248 10 slow variancesmall distance between p50 and p99and:Unstable runnermean \u2248 10 shigh variancefast medianlarge p95very large p99This is the important part of the experiment.I am not changing average runtime deliberately.I am changing uncertainty.For the stable runner, I can approximate execution time with something close to a narrow normal distribution centered near ten seconds.For the unstable runner, a mixture model works better:T =    fast distribution, with high probability    slow distribution, with low probabilityFor example:P(fast) = 0.8P(slow) = 0.2The exact coefficients are not the conclusion.They are knobs.What matters is that the second distribution has the same approximate mean but a much longer right tail.Once I compare them through p50, p95 and p99 rather than mean alone, the difference becomes obvious.The stable runner is mediocre every time.The unstable runner is excellent most of the time and terrible just often enough to destroy confidence.That trade-off appears constantly in real systems.A build step usually finishes quickly, except when dependency resolution behaves differently.An integration test normally takes a few seconds, except when a container cold-starts.A local database query is fast until something invalidates a cache.A compiler is predictable until incremental state disappears.From the perspective of the average, these events are noise.From the perspective of the developer, they are remembered.4. Developer Behavior Has MemoryA simple latency model assumes every test run is independent.I do not think developer behavior works that way.Suppose five recent runs took:91011109Now compare that with:2182189The average is similar.My expectation is not.After the first sequence, I trust the tool.After the second, I know that another long delay can appear without warning.That memory affects the next run.Even if the sixth test completes in three seconds, I may leave after two because the previous history has trained me not to wait.Now a fast test can produce a slow feedback loop.The test itself did nothing wrong.Its predecessors changed my behavior.A simple way to represent recent expectation is an exponentially weighted estimate:E_n = \u03b1T_n-1 + (1 &#8212; \u03b1)E_n-1Recent runs receive more weight than old ones.But expected duration alone is still not enough.The developer also learns variance.A more useful internal state would contain at least:expected durationuncertaintyrecent extreme eventsThis matters because outliers can have a delayed cost.A thirty-second test does not necessarily waste only thirty seconds.It can make me switch earlier during the next several runs.The latency penalty leaks forward.That is difficult to see in CI dashboards because test systems measure executions independently.Humans do not experience them independently.5. Adding Context Switching to the ModelI then added a simple decision rule.At each point during a wait, the simulated developer can remain in the task or leave the current context.The switching probability depends on three variables:elapsed timeexpected remaining timeuncertaintyConceptually:P(switch) =    sigmoid(        a        + b1 \u00d7 elapsed        + b2 \u00d7 expected_remaining        + b3 \u00d7 uncertainty    )The coefficients are not empirical claims.They only provide a mechanism for comparing distributions.The result I care about is qualitative.With stable tests, uncertainty remains low. Even when execution is not especially fast, waiting is relatively easy to justify.With unstable tests, a few long runs increase the perceived uncertainty of future runs.Switching probability rises earlier.This creates situations like:run 1   3 srun 2   4 srun 3   32 srun 4   3 srun 5   4 sRun 4 is technically fast.But after run 3, I may leave the IDE after two seconds.Run 4 completes one second later.I do not notice immediately because I am already somewhere else.Then I return.Then I reconstruct the original code context.A three-second test has just produced a feedback loop lasting much longer than three seconds.The test duration is not the problem.Trust in the timing model is.6. Effective Feedback Latency Is Larger Than Test LatencyThis suggests separating two metrics.The first is obvious:T_testThe second is what I actually experience:T_effectiveA simple decomposition is:T_effective =    T_test    + T_detection    + T_reconstructionT_detection is the delay between test completion and me noticing it.T_reconstruction is the time required to resume meaningful work on the original problem.If I stay inside the IDE, both values may be close to zero.If I switch context, both can become substantial.Now variance becomes expensive in a second way.A long-tailed runner creates more context switches.More context switches increase detection delay.More detection delay increases effective feedback latency.Higher effective&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-493404","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/493404","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=493404"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/493404\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=493404"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=493404"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=493404"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}