---
title: 'Tracing Performance Problems'
source: 'https://academia.sh/en/courses/system-administration/tracing-performance-problems'
course: 'System Administration'
language: en
updated: '2026-08-17T18:10:04+00:00'
license: 'CC BY-SA 4.0'
---

# Tracing Performance Problems

Separating CPU, I/O, and memory bottlenecks: a view producing four times as many lines is still wrong on 12 of 18 events, a three-line view drops to 2, and the reading order of the same three metrics removes the remaining two misses.

The previous lesson took up indicators that reduce a machine's state to
single numbers and measured that the reduction eats the spike. One gap
remained there: load average collected processes waiting on the processor
and processes waiting on disk **inside the same number**. When a machine
slows down, the question asked requires exactly separating that sum: is
the slowdown caused by the processor, the disk, or memory.

This lesson takes up separating the three bottleneck types. The expected
solution is more detailed output: more columns, more lines, finer detail.
What is measured is whether this expectation is correct, and the course's
second claim is paid here: **more output does not mean a better
diagnosis.**

Where the expectation comes from is understandable too. Facing a failure,
the only control knob at hand is mostly the level of detail: a longer
list, a finer breakdown, more frequent sampling. All of these grow the
output, and growing output gives a feeling that effort is increasing. The
measurement tests this feeling, and the result is clear: if the direction
of the effort is wrong, its amount does not change the outcome.

## Three Bottlenecks and Their Shared Symptoms

A machine slowing down can arise from three mechanisms, and the three look
the same to the user.

In a **CPU bottleneck**, the number of runnable processes exceeds the
number of processors; processes queue up, each waiting for its turn.
Processor busy percentage is high, and that high value corresponds to
real work.

In an **I/O bottleneck**, processes wait not on the processor but on
disk. While waiting, they are in uninterruptible wait; they do not
consume the processor but they are counted. What shows up on the
processor side is the **wait** share: the processor is idle, but the
reason it is idle is not that no work is left but that data has not
arrived. A view that reports busyness by folding the wait share into it
cannot tell this situation apart from a CPU bottleneck.

In a **memory bottleneck**, physical memory runs short and pages are
moved to swap. The trap here is two-fold. First, swap movement is itself
a disk operation; heavy swapping **produces I/O wait**, and the memory
bottleneck looks like a disk bottleneck. Second, memory indicators stay
high not just during the pressure but after it has passed too: pages that
went out to swap sit there until recalled.

There is a fourth possibility, and it falls outside this trio: the
bottleneck can be not in the machine itself but in a counterpart it is
waiting on. A process waiting on a network request is slow too, but it
leaves no trace in any of the three metrics; the processor is idle, the
disk is idle, memory is comfortable. This lesson does not measure that
situation, and it is the one scenario where the machine is slow even
though all three metrics say "normal." Diagnosis on the network side is
the subject of the next course.

The Operating System Concepts course built the mechanism of paged memory
and scheduling; it is not repeated here. The question here is not the
mechanism but **which mechanism the operator can read from which
output.** The same distinction was set up at the opening of this course:
measuring an abstraction's cost and measuring whether the operator can
see that abstraction are two separate jobs.

## Five Views

The measurement compares five separate views, and each produces a
different number of lines.

**A — process list.** Twenty-four lines, one meaningful column: CPU
percentage per process. This is the most common first look.

**B — thread detail.** The same list, expanded to thread level:
ninety-six lines. Four times as much output, **the same columns**. The
level of detail has gone up; the metric set has not changed.

**C — A plus a wait column.** Twenty-four lines again, but the
per-process I/O wait share has been added to the list. The line count is
the same as A, the metric set is one unit larger.

**D — three system metrics.** Three lines: CPU busy, wait share, swap
movement. No per-process detail at all.

**E — the same three metrics, a different reading order.** The exact
same three lines as D; the only difference is which metric is checked
first. D looks at wait share first, E at swap movement.

The dump below is an example and has not been run; it shows the three
metrics view D carries.

```text
# example dump , not run
$ vmstat 1 1
 r  b   swpd    free  ...  si  so  ...  us  sy  id  wa
 4  6  102400  81920  ...  38  44  ...  61  12   5  22
```

## Where These Three Metrics Are Read From

Each of the three metrics comes from a separate counter, and which tool
reads them is a secondary question; what matters is the counter itself.

**CPU busy** is computed from the time buckets the kernel keeps: time
spent in user mode, time spent in kernel mode, idle time, and time spent
waiting. These buckets are **cumulative** counters increasing since boot;
the instantaneous percentage comes from the difference between two
readings. A single reading gives the average since boot, and that average
says almost nothing during a failure. This is another form of the lag
problem from the previous lesson: the counter itself does not lag, but a
single reading **averages everything**.

**Wait share** is the name of one of the same buckets, and it means
exactly "the processor was idle, but at least one process was waiting on
disk." Two warnings are needed. First, this is a duration measure, not a
queue measure; it does not say how many processes are waiting. Second, if
the processor is genuinely busy, wait share **reads low**, because the
processor has no idle time. That is, when the processor and the disk hit
a bottleneck at the same time, wait share understates the size of the
problem.

**Swap movement** is counted in two directions: pages read back into
memory, and pages written to disk. The distinction matters. Writing to
disk alone is not evidence of pressure; the operating system can move
pages untouched for a long time out to disk even while things are
comfortable. The evidence of pressure is pages being **read back**: a
page in use was evicted and immediately requested again. A diagnosis that
looks only at whether the swap area appears full searches the present for
a pressure event that happened and ended in the past.

## What Is Measured: Line Count or Metric Set

The setup's assumptions: eighteen events are generated, and each event
has a single real cause (**GN19**); the three metrics' bands are derived
from the cause (**GN20**); once swap movement rises above sixty, memory
pressure also produces I/O wait (**GN21**); the views' lines-per-event
counts are 24, 96, 24, 3, and 3 (**GN22**); the decision thresholds are
60 for CPU, 25 for wait, 20 for swap (**GN23**).

The oracle is the real bottleneck cause of the eighteen events placed
into the setup. The tool output is the line count each of the five views
produces. The diagnosis is the cause each view derives from its own
metrics; wrong diagnosis is the count of diagnoses that come out
different from the real cause.

```python
SEED = 20260218


def generator(seed):
    d = seed

    def next_val(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return next_val


CAUSE = ("cpu", "io", "memory")


def bottlenecks(seed=SEED, count=18):
    """GN19 ORACLE: every event's single real bottleneck. GN20: the three metrics
    are derived from the cause. GN21: swap over 60 also produces wait."""
    r = generator(seed)
    events = []
    for i in range(count):
        n = CAUSE[r(3)]
        if n == "cpu":
            cpu, swap, wait = 78 + r(18), r(3), 2 + r(4)
        elif n == "io":
            cpu, swap, wait = 74 + r(20), r(3), 46 + r(28)
        else:
            cpu, swap = 52 + r(30), 34 + r(40)
            wait = 30 + r(20) if swap >= 60 else 6 + r(9)
        events.append({"no": i + 1, "cause": n, "cpu": cpu,
                        "wait": wait, "swap": swap})
    return events


def view_a(o):
    """GN23 thresholds. Process list , single metric: CPU percentage."""
    return "cpu" if o["cpu"] >= 60 else "memory"


def view_c(o):
    """Same list , one more column: wait."""
    if o["wait"] >= 25:
        return "io"
    return "cpu" if o["cpu"] >= 60 else "memory"


def view_d(o):
    """Three system metrics , wait read first."""
    if o["wait"] >= 25:
        return "io"
    return "memory" if o["swap"] >= 20 else "cpu"


def view_e(o):
    """Same three metrics , swap read first."""
    if o["swap"] >= 20:
        return "memory"
    return "io" if o["wait"] >= 25 else "cpu"


VIEW = (("A process list", view_a, 24),        # GN22: lines/event
        ("B thread detail", view_a, 96),
        ("C A + wait column", view_c, 24),
        ("D three system metrics", view_d, 3),
        ("E same three metrics , swap first", view_e, 3))

for seed in (SEED, 20260219):
    O = bottlenecks(seed)
    print("seed", seed, "| events", len(O), "|",
          {n: sum(1 for o in O if o["cause"] == n) for n in CAUSE})
    print("  view                           lines/event  total lines  wrong diagnosis")
    for name, f, lines in VIEW:
        y = sum(1 for o in O if f(o) != o["cause"])
        print(f"  {name:30s} {lines:10d} {lines * len(O):13d} {y:12d}/{len(O)}")
```

```
seed 20260218 | events 18 | {'cpu': 4, 'io': 4, 'memory': 10}
  view                           lines/event  total lines  wrong diagnosis
  A process list                         24           432           12/18
  B thread detail                        96          1728           12/18
  C A + wait column                      24           432            8/18
  D three system metrics                  3            54            2/18
  E same three metrics , swap first          3            54            0/18
seed 20260219 | events 18 | {'cpu': 7, 'io': 6, 'memory': 5}
  view                           lines/event  total lines  wrong diagnosis
  A process list                         24           432           11/18
  B thread detail                        96          1728           11/18
  C A + wait column                      24           432            5/18
  D three system metrics                  3            54            2/18
  E same three metrics , swap first          3            54            0/18
```

## What the Output Says Versus the System's Truth

Three numbers sit side by side. **The oracle:** four of the eighteen
events are CPU-caused, four I/O-caused, ten memory-caused. **The tool
output:** the views produce, in order, 432, 1728, 432, 54, and 54 lines.
**Wrong diagnosis:** 12, 12, 8, 2, and 0.

Comparing the first row against the second pays off the claim. View B
produces exactly **four times** as many lines as A — 1728 instead of 432
— and the wrong-diagnosis count **does not change at all**: it stays at
twelve. Raising the level of detail grew the text to be read; it
corrected the diagnosis by not one bit. The reason is clear: the thread
breakdown is the same metric sliced finer, and slicing a metric finer
cannot see what that metric could not see in the first place.

The third row shows what makes the difference. View C produces a quarter
as many lines as B — the same as A, 432 — but wrong diagnosis drops from
12 to **8**. What produces the gain is not the line count but **a single
column** added to the list: wait share. The correction of four wrong
diagnoses comes from I/O bottlenecks becoming separable from CPU
bottlenecks.

The fourth row takes this to the extreme. View D produces **54 lines**; a
thirty-second of B. Wrong diagnosis drops to **2**. Three lines carrying
no per-process detail at all give six times better diagnosis than a view
carrying ninety-six lines per process. This is the measurement's name:
**what separates them is not the line count but which metric is looked
at.**

The gap between C and D also shows where scale fits in. C looks per
process and can answer "which process"; D looks at the system as a whole
and cannot answer that question. Yet D is wrong less often, because the
question being asked is not "which process" but "which resource." The two
questions are frequently mixed up: searching for the process consuming
the most resource without first finding the source of the slowdown often
ends up finding **the victim**. On a machine under memory pressure, the
process waiting the most is not the process creating the pressure; it is
the one most affected by it.

Choosing a view also has a consequence for order. The correct sequence is
to look at the system as a whole first, then the process: D says
"memory," and then the process list is sorted by memory and the culprit
is sought. The reverse sequence — process list first, system metric
second — is represented in the measurement by row A, and it takes the
wrong path on twelve of eighteen events.

## The Remaining Two Misses and Reading Order

The fifth row pays off the remaining share. View E carries **the exact
same three metrics** as D; the only difference is which one is asked
first. D looks at wait share first and says "I/O" when it sees high
wait. E looks at swap movement first and says "memory" when it sees high
swap. Wrong diagnosis is 2 for D, **0** for E.

The events producing the difference are memory bottlenecks whose swap
rises above sixty. There, because swap movement itself creates disk
work, wait share rises too; the two metrics are high at the same time,
and which one is read first determines the diagnosis. Wait share is a
**result**, swap movement is a **cause**; a reading order that reads the
result first shadows the cause.

This yields a rule for the operations side: if the causal direction
between two metrics read high at the same time is not known, growing the
metric set is not enough. Even an operator looking at just three numbers
is wrong on two of eighteen events if they look in the wrong order. The
metric set determines the diagnosis's **ceiling**; reading order
determines whether that ceiling is reached.

There is a general way to decide which metric a sequence should start
with: the metric **highest in the chain** is read first. Swap movement is
the direct result of memory pressure and arises from no other cause;
wait share, on the other hand, can arise from disk slowness, from swap,
or from an ordinary bulk read. A metric with multiple causes is read
**after** a metric with a single cause. This rule is not specific to the
setup; it works in every metric set where indicators feed into one
another.

The decision tree has a cost too. A procedure that reads the three
metrics in a fixed order reduces a situation where two bottlenecks
genuinely exist **at the same time** to a single cause; it stops at the
first matching branch and never reports the second cause. This does not
show up in the measurement, because in the setup every event has a
single real cause. On a real machine, reducing the diagnosis to a single
name is itself a kind of wrong diagnosis.

The limit of the measurement has to be written clearly too. E's zero
comes from the setup being perfectly separable; a real machine is not
expected to have three metrics separate every event. What is meaningful
is not the zero but **the sequence from 12 down to 0**: quadrupling the
line count gained nothing, adding the right metric removed four misses,
narrowing and correcting the metric set removed ten misses, and fixing
the order removed the remaining two.

## The Second Seed

In the second seed, the distribution of events changes: seven CPU, six
I/O, five memory. Views A and B again give **the exact same** number —
eleven wrong diagnoses each — and the line-count difference again
changes nothing. C drops to **5**, D to **2**, E to **0**. The absolute
numbers **depend on the setup**; what does not depend on the setup is
that quadrupling the line count never changes wrong diagnosis, and that
the three-line view outperforms the ninety-six-line view in both seeds.

## Summary

- The three bottleneck types produce the same symptom; memory pressure
  also creates I/O wait through swap, and looks like a disk bottleneck.
- Quadrupling the line count (from 432 to 1728) **never changes** wrong
  diagnosis: it stays the same in both seeds.
- Adding a single correct column to the list brings wrong diagnosis from
  12 to **8**; dropping process detail and reducing to three system
  metrics brings it to **2**.
- Changing the reading order of the same three metrics removes the
  remaining two misses too; wait share is a result, swap movement is a
  cause.
- What determines the diagnosis is not the volume of output but the
  metric set and the order that set is read in.

## Next Step

What was measured throughout this topic was how much of the truth the
text and numbers a machine produces about itself carry: a diagnosis
looking at a single unit file was wrong on five of six, rotation deleted
81 of 86 errors, an averaged indicator never showed a fifty-four-second
spike at all, and four times the output corrected not a single miss. The
silent assumption behind all these measurements was that data could be
written to disk. The next topic removes that assumption and descends
into storage: block devices, partitions, file systems, and a file
system's two independent limits that run out. There, the situation where
a disk is one percent full and yet a write fails is shown numerically.
