--- a/heartbeat.c 2026-09-23 13:37:17 +++ b/heartbeat.c 2026-09-23 13:37:17 @@ -1,23 +1,22 @@ -/* EXERCISE 04 — BROKEN ON PURPOSE. Do not copy this shape into real code. +/* EXERCISE 04 — REPAIRED. * * A media app. One latency-sensitive thread refreshes a level meter: it does a * small, fixed amount of work, over and over, and must finish each unit before * the next frame is due. A background library re-indexes the user's collection - * on a pool of CPU-bound threads. Both were written by reasonable people; - * the pool declares no quality of service, so it competes in the same - * scheduling band as the meter. + * on a pool of CPU-bound threads. * - * Symptom as reported by QA: "the meter judders while a re-index runs. But we - * profiled it — the meter's average frame cost is completely normal, nothing - * is blocked, and there is no lock anywhere near it." + * The repair: the pool declares QOS_CLASS_BACKGROUND, as its first act on each + * thread. That is not a speed dial. It is a PLACEMENT decision: it moves the + * pool into a lower scheduling band so it stops taking turns against the work + * the user can see. The re-index still finishes; it simply yields the tail. * * Build and run: - * clang -O2 -g -Wall -Wextra -pthread heartbeat.c -o /tmp/heartbeat_broken - * /tmp/heartbeat_broken + * clang -O2 -g -Wall -Wextra -pthread heartbeat.c -o /tmp/heartbeat_fixed + * /tmp/heartbeat_fixed * - * Nothing here deadlocks and nothing here races. No thread ever enters the - * BLOCKED state. The defect is a scheduling decision that was never made, and - * it is invisible in the mean. + * Read hogBatches as well as the latency figures: this repair buys tail + * latency by giving the pool less CPU, and pretending otherwise would be + * dishonest. That trade is the answer to "what does your fix cost?". * * The fixture measures itself twice — once with the pool idle, to establish * this machine's floor, and once with the pool running — so every number it @@ -58,10 +57,13 @@ static atomic_int stop_hogs = 0; static atomic_ullong hog_batches = 0; -/* The re-indexing pool. It declares no quality of service, so every one of - * these threads competes in the same band as the meter thread. */ +/* The re-indexing pool. Each thread declares its own quality of service as + * its first act, so the scheduler knows what this work is worth before it + * places the thread. QoS is per-thread: setting it on the spawning thread + * would not have travelled here. */ static void *hog(void *unused) { (void)unused; + pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0); unsigned long long n = 0; volatile double x = 0; while (!atomic_load_explicit(&stop_hogs, memory_order_relaxed)) { @@ -101,8 +103,9 @@ int ncpu = (int)sysconf(_SC_NPROCESSORS_ONLN); int nhogs = ncpu * HOG_MULT; - /* The meter thread is this one. Ask for the highest ordinary class so the - * intent is on record — and notice, below, that asking is not enough. */ + /* The meter thread is this one. Asking for the highest ordinary class is + * necessary but not sufficient: it only means something once the work it + * competes with has declared something lower. */ pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0); double base50, base99, baseMax; @@ -120,7 +123,7 @@ printf("cores=%d\n", ncpu); printf("hogThreads=%d\n", nhogs); - printf("hogQos=unspecified\n"); + printf("hogQos=background\n"); printf("units=%d\n", UNITS); printf("idleP50Us=%.1f\n", base50); printf("idleP99Us=%.1f\n", base99);