Frontend Labs
⋯

Part 18 -- Capstone projects

Reading about OS internals, Linux commands, and Kubernetes gets you to "I understand this." Building something end to end, and then deliberately breaking it and fixing it, gets you to "I can do this under pressure," which is the actual bar for a beginner SRE or backend engineer role. These four projects are designed to be done in order -- each one assumes the skills from the previous one and the previous parts of this guide.

There is no single "correct" solution to any of these. What matters is that you can explain every decision you made, in the vocabulary this guide has given you: syscalls, page cache, cgroups limits, readiness probes, image layers.

Chapter 62 -- Project 1: build and profile a small program, OS-level

In one sentence

Write a small program that does real work, then use the OS-level tools from Parts 1-6 (strace, time, /proc, ulimit, top) to explain, in kernel terms, exactly where its time and memory go.

Why this matters

This project exists to make the abstract concrete: "page fault," "context switch," and "syscall overhead" stop being vocabulary words and become numbers you personally measured on your own machine. This is also close to the actual daily work of performance engineering -- "why is this slow" is answered by measurement, not by guessing, and the tools here are the same ones used to debug production latency issues at any company running Linux.

How it actually works

The project, concretely:

   STEP 1 -- Write a program (any language) that:
     a) reads a large text file (a few hundred MB -- e.g. concatenate
        some log files, or download a public dataset)
     b) counts word frequency (a classic, CPU + I/O mixed workload)
     c) writes the top 20 words to stdout

   STEP 2 -- Measure it plainly first:
     /usr/bin/time -v ./wordcount bigfile.txt
     Read every line of the output: elapsed time, user time, system
     time, maximum resident set size, major/minor page faults,
     voluntary/involuntary context switches. Write down what each
     number means in your own words, using Chapters 4-13's vocabulary.

   STEP 3 -- Trace its syscalls:
     strace -c ./wordcount bigfile.txt
     Which syscall dominates the count? Which dominates total time?
     (Usually `read` calls dominate count; for a naive line-by-line
     reader, this can be enormous -- tens of thousands of small reads
     instead of a handful of large ones.)

   STEP 4 -- Change the I/O strategy and re-measure:
     Rewrite the file-reading part to read in large chunks (e.g. 1MB
     buffers) instead of line-by-line with an unbuffered reader, or
     use memory-mapping (mmap) instead of read(). Re-run STEP 2 and
     STEP 3. Compare: syscall count, elapsed time, major page faults.

   STEP 5 -- Constrain it with cgroups and watch it suffer honestly:
     systemd-run --scope -p MemoryMax=50M ./wordcount hugefile.txt
     If the file is bigger than 50MB and your program loads it all
     into memory at once, watch it get OOM-killed. Check `dmesg` or
     `journalctl -k` for the OOM killer's log line (Chapter 11).
     Then fix the program to stream the file instead of loading it
     all at once, and show it now completes under the same limit.

Real-world example

This exact exercise -- profile, find the surprising bottleneck, fix it, re-measure -- mirrors one of the most famous real performance investigations in systems literature: the discovery that naive line-by-line I/O (calling read() or getline() once per line) can be 10-100x slower than buffered, chunked reads, purely because of syscall overhead, not because of disk speed. This is precisely the kind of finding Brendan Gregg documents extensively in his USE Method and "Systems Performance" methodology (used at Netflix, and taught widely in the SRE community): most performance problems are not "the CPU is too slow," they are "the program is making the kernel do far more work than the task actually requires," and the only way to see that is to measure syscalls and page faults directly instead of guessing from wall-clock time alone.

Try it yourself

This chapter's project is the "try it yourself" -- work through Steps 1 through 5 above on your own machine (a Linux VM or container is fine; systemd-run cgroup limiting won't work in plain macOS/Windows, but Docker's --memory flag gives you the equivalent, see Chapter 47).

Common mistakes

  • Measuring only wall-clock time (time ./program) and stopping there -- wall-clock time alone doesn't tell you why it's slow, only that it is.
  • Optimizing before measuring -- rewriting code based on a guess about what's slow, then finding the real bottleneck was somewhere else entirely.
  • Running the "before" and "after" measurements under different conditions (different file, different machine load, cache warm vs cold) and drawing conclusions from a comparison that isn't apples-to-apples. Run each version multiple times and note whether the file is already in the page cache (Chapter 15) from a previous run.

Check yourself

  1. If /usr/bin/time -v shows a large number of "minor" page faults but zero "major" page faults, what does that tell you, and should you be worried?
  2. Why would switching from line-by-line reads to 1MB chunked reads reduce the syscall count by orders of magnitude, and why does that matter even though both approaches read the same total bytes?
  3. Your program gets OOM-killed under a 50MB memory limit even though the input file is only 30MB. What are two plausible reasons memory usage could exceed the file size?

Further reading

  • Brendan Gregg, "Systems Performance: Enterprise and the Cloud," 2nd edition (2020) -- especially the chapters on methodology and the USE Method
  • man 1 time, man 1 strace, man 1 systemd-run

Generated from real-life-os-guide.md · 8 Oct 2026 16:36