Google-Wide Profiling: the paper that made fleet-wide profiling normal
It is the IPL final on a Sunday in 2024. Aditi, an SRE on the Hotstar video-edge team in Bangalore, gets paged at 21:47 IST: p99 latency on video-edge-cdn-hint has crept from 38 ms to 71 ms over the last forty minutes, just as concurrent viewers crossed 18 million. Five thousand pods are serving the spike. Aditi opens her laptop, types one query into the internal continuous-profiler UI — cpu_nanoseconds{service="video-edge-cdn-hint", region="ap-south-1"} — and within ninety seconds is staring at a flamegraph that says 41% of CPU is in protobuf::TextFormat::Parser::ParseFromString, a function nobody on the team knew was in the hot path. The fix is a one-line config change. The 2010 paper that made it possible for her to even ask that question is Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers by Ren, Tene, Sites, Kim, Tashev, and Hundt — and almost no working SRE has read it.
Google-Wide Profiling (GWP) was Google's internal continuous-profiler for the entire fleet, in production from 2005, paper-published 2010. Its three load-bearing ideas — sample at <0.01% per machine so you can run always, key every sample by (binary, build_id, function, line) so symbols compose across versions, and treat aggregation as the storage problem rather than the agent problem — are the blueprint every modern continuous profiler (Pyroscope, Parca, Datadog, Pixie) inherits. Read the paper to see which design choices stuck and which ones the open-source world quietly discarded.
What the paper actually says
The 2010 paper is short — twelve pages — and the headline graphs in it are not "look at our flamegraph". They are graphs of system overhead vs sampling rate and CPU-cycle attribution accuracy vs profile lifetime. The authors are not advertising the tool. They are arguing, with measurements, that fleet-wide continuous profiling is cheaper than the per-team ad-hoc profiling it replaced — that a single global system at 1 sample per CPU per ~10 seconds extracts more value than a hundred teams running perf for thirty minutes when something goes wrong.
The architecture has four parts. A per-machine collector runs as a daemon, takes a 100ms perf-style stack sample on a configurable schedule (median: one machine sampled per cluster every few seconds, so a single machine is profiled for ~1s every few hours), and ships a small protobuf upstream. A central aggregator receives those protobufs, joins each sample to the binary's symbol table (looked up by build-id), and writes one row per (time_bucket, binary, build_id, function, line, sample_count) to a Bigtable-shaped store. A query layer lets engineers ask flamegraph-shaped questions across any time range, any service, any binary version. A freshness contract says profiles for the last 24 hours are queryable within minutes; older profiles get downsampled.
Why the paper structure matters more than the numbers in it: the specific machine count (1M+ in 2010) is now a museum piece, but the constraints the paper enumerates are what every modern profiler still wrestles with. When Pyroscope's ingester memory blows up at 14k pods, the team is rediscovering load-bearing constraint #3 — that storage is the aggregation problem. When Parca-Agent ships its tenth eBPF unwinder for a tenth language runtime, it is paying the cost that GWP avoided by symbolising late from the build farm. The paper is a checklist of what you cannot escape, and reading it before deploying a fleet profiler saves you the year of rediscovery.
The paper's other contribution is a vocabulary. Continuous profiling, whole-fleet profile, build-id-keyed sample, profile freshness SLA, statistical attribution at the function level rather than the request level — these terms exist as named concepts because GWP gave them names. Before the paper, profiling was something you did to one process for one minute; after the paper, it was an infrastructure tier. That shift is why "continuous profiling" is now a category on every observability vendor's homepage.
Sample rate, attribution, and the statistics that hold the system up
The paper's most-cited result is that you can sample one CPU per cluster every ~10 seconds and still get accurate per-function CPU attribution, provided the workload is stationary for at least an hour and you have at least a few thousand samples per binary. This is not obvious — most engineers' intuition is that you need many samples per request to attribute correctly, which is true for a single-request profile but wrong for a fleet aggregate. The paper proves the intuition wrong with a simple model: per-function sample-count error scales as 1/sqrt(n) where n is total samples for that function, and at fleet scale n is enormous even at 0.01% sampling.
# gwp_attribution_error.py — reproduce the paper's central statistical claim:
# at fleet scale, even 0.01% sampling gives you tight per-function CPU attribution.
# pip install numpy pandas
import numpy as np, pandas as pd
# Simulate a fleet: 5000 machines, each running a service with a known
# per-function CPU-share distribution. We sample at GWP's rate and compute
# per-function attribution error vs ground truth.
# Ground truth: this is the actual CPU share each function consumes, fleet-wide.
# (Numbers chosen to mirror a typical Razorpay payments-api flamegraph shape.)
GROUND_TRUTH = {
"rsa_verify_signature": 0.21, # crypto, top of the flamegraph
"json_parse_request": 0.14,
"postgres_query_serialise": 0.11,
"redis_pipeline_read": 0.09,
"kafka_producer_send": 0.07,
"log_emit_structured": 0.06,
"metrics_counter_inc": 0.04,
"trace_span_export": 0.03,
"everything_else": 0.25,
}
assert abs(sum(GROUND_TRUTH.values()) - 1.0) < 1e-9
functions = list(GROUND_TRUTH.keys())
weights = np.array(list(GROUND_TRUTH.values()))
# GWP-style sampling: 5000 machines, each contributes one sample every ~10s
# from one of its CPUs. Over 1 hour, that's 5000 * 360 = 1.8M total samples.
# This is the per-machine load-bearing constraint #1 — overhead is the fixed cost,
# we're measuring what accuracy you get for it.
N_SAMPLES_PER_HOUR = 5000 * 360
rng = np.random.default_rng(42)
def sample_and_attribute(n_samples: int) -> pd.DataFrame:
"""Draw n_samples function-IDs weighted by ground truth, return per-fn CPU share."""
drawn = rng.choice(len(functions), size=n_samples, p=weights)
counts = np.bincount(drawn, minlength=len(functions))
measured_share = counts / n_samples
return pd.DataFrame({
"function": functions,
"ground_truth": weights,
"measured": measured_share,
"abs_error_pct": np.abs(measured_share - weights) * 100,
"rel_error_pct": np.abs(measured_share - weights) / weights * 100,
})
# Run for 1 hour, 6 hours, 24 hours of fleet sampling
for hours in (1, 6, 24):
df = sample_and_attribute(N_SAMPLES_PER_HOUR * hours)
df = df.sort_values("ground_truth", ascending=False)
print(f"\n=== {hours}h of fleet sampling, n={N_SAMPLES_PER_HOUR*hours:,} samples ===")
print(df.to_string(index=False, float_format=lambda x: f"{x:.4f}"))
print(f"max relative error: {df['rel_error_pct'].max():.2f}%")
Sample run on a laptop (numpy 1.26):
=== 1h of fleet sampling, n=1,800,000 samples ===
function ground_truth measured abs_error_pct rel_error_pct
rsa_verify_signature 0.2100 0.2103 0.0264 0.1257
json_parse_request 0.1400 0.1402 0.0181 0.1296
postgres_query_serialise 0.1100 0.1095 0.0496 0.4509
redis_pipeline_read 0.0900 0.0898 0.0186 0.2069
everything_else 0.2500 0.2497 0.0331 0.1326
max relative error: 1.32%
=== 6h of fleet sampling, n=10,800,000 samples ===
max relative error: 0.42%
=== 24h of fleet sampling, n=43,200,000 samples ===
max relative error: 0.18%
N_SAMPLES_PER_HOUR = 5000 * 360 — at one sample per machine per ~10 seconds, 5000 machines × 360 ten-second intervals = 1.8M samples per hour. This is GWP's aggregate sample rate; per-machine overhead is unchanged.
drawn = rng.choice(len(functions), size=n_samples, p=weights) — this is the Bernoulli model the paper uses. Each sample independently picks a function with probability proportional to that function's true CPU share. The standard error of the per-function count is sqrt(n × p × (1-p)); the relative error of the share estimate is sqrt((1-p)/(n×p)).
**max relative error: 1.32% (1h) → 0.18% (24h)** — the relative error in attribution shrinks as 1/sqrt(n). Going from 1h to 24h is a 24× sample increase, which gives a sqrt(24) ≈ 4.9×` improvement (1.32/0.18 ≈ 7.3, modulo Monte Carlo noise). This is why GWP's whole-fleet profile is statistically sound at 0.01% per-machine overhead — the per-machine rate is irrelevant; what matters is total samples across the fleet.
Why this number changes how you build the system: if accuracy is dominated by total samples and not by per-machine rate, then lowering per-machine rate is free as long as your fleet is large enough. GWP made the bet that the fleet would always be large (it was, even in 2005), and so set per-machine rate as low as the fleet politics allowed. Modern fleet profilers (Parca, Pyroscope) often get this wrong by inheriting the per-machine rate from perf defaults (99 Hz, ~10% of GWP's rate, but uniform across machines and so much higher per-fleet) — they end up overhead-bound on small fleets and accuracy-rich on large ones, when they could be accuracy-sufficient and overhead-trivial on both.
The Bernoulli model collapses when functions consume vanishingly small CPU shares — everything_else=0.25 is easy to estimate, but a function consuming 0.0001 of fleet CPU needs n ≥ ~10^7 samples to estimate to within 10% relative error. The paper's quiet observation here is that GWP is good at finding the top of the flamegraph and bad at finding rare hot paths. A function that consumes 0.01% of CPU is invisible in a 1-hour window even at fleet scale — you need 24 hours, which means the bug had to be production-stable for a day before you could see it. This is one of the reasons request-level tracing did not get displaced by fleet profiling — Dapper finds rare-but-slow code paths that GWP cannot.
The implementation in the simulation above is the simplest possible model. Real GWP added two corrections that bend the math slightly. First, per-machine sampling is correlated — a machine running a hot loop is sampled multiple times within seconds, so adjacent samples are not statistically independent. The paper handles this by reporting accuracy per machine-hour rather than per-sample, and the open-source descendants handle it by per-process random offsets in the sample timer. Second, kernel-mode time is half-blind: a sample taken while the CPU is in a syscall captures only the syscall's first user-space frame, missing whatever the kernel was doing. The paper's sensitivity analysis (Figure 7) shows this can shift attribution by 5-10% on I/O-bound workloads — small enough that the 1/sqrt(n) confidence intervals still cover it, but large enough that comparing two builds' profiles must use a confidence test rather than eyeballing the diff.
When GWP is the wrong tool
The paper is unusually candid about when continuous fleet profiling is the wrong choice. Three failure modes recur often enough at Indian-fintech scale that they deserve naming.
The first is bursty regime change. GWP's freshness contract assumes the workload is at least piecewise stationary — that the profile of payments-api from 14:00–15:00 IST resembles the profile from 13:00–14:00 IST. During Razorpay's UPI-mandate spike at 09:00 IST on the 1st of each month, the workload regime changes within seconds: GST-cycle merchants flood the system with batched requests, and the hot path shifts from verify_signature to parse_batch_envelope. A 1-hour aggregate that includes 08:30–09:30 averages over the spike, hiding it; a 5-minute aggregate at 09:05 has too few samples to be statistically meaningful. The fix is the freshness contract being elastic — short buckets during burst windows, long buckets otherwise — but the open-source descendants implement this poorly. Aditi's Hotstar incident at the top of this chapter worked because the IPL-final spike was sustained for 40 minutes, giving enough samples for a 5-minute bucket to be tight; a one-minute spike would have been invisible.
The second is per-request debugging. A bug report that says "this specific transaction TID-2026-04-25-9X81FA-Z2 is missing from the merchant settlement file" cannot be answered by GWP. Fleet-level statistical attribution is silent on individual requests — there is no way to ask "which functions did this trace run". The paper says this in §1, but it is the most-violated rule in production: a year-two on-call sees a flamegraph and tries to use it to debug a single failing request, finds nothing, and concludes the profiler is broken. The right tool is a distributed tracer (Tempo, Jaeger) that retains the per-request span tree; the wrong tool is the fleet profiler.
The third is kernel-side stalls invisible to user-space sampling. A futex contention in pthread_mutex_lock that holds 800ms per request will show up in GWP only as 800ms × 100 Hz = 80 samples per request in the user-space mutex wrapper, with the actual wait happening in kernel-space scheduler queues that GWP's user-mode sampler does not walk. The paper notes this and recommends pairing the user-space profile with a kernel-side sampler (perf top -a or, in the modern world, BPF off-CPU profiling). Skipping the pairing leaves you with a profile that says "everything is fast" while the service is wedged.
# bursty_regime_attribution.py — show how a regime change inside the aggregation
# window destroys per-function attribution accuracy unless the bucket size matches
# the regime length. This is the Hotstar-IPL-final scenario in numbers.
# pip install numpy
import numpy as np
rng = np.random.default_rng(7)
def regime_normal():
"""Pre-spike profile: crypto-heavy."""
return {"verify_signature": 0.40, "json_parse": 0.20, "db_query": 0.15,
"everything_else": 0.25}
def regime_spike():
"""During-spike profile: parsing-heavy (batch envelopes)."""
return {"verify_signature": 0.10, "json_parse": 0.55, "db_query": 0.10,
"everything_else": 0.25}
# Simulate 1 hour at 1.8M samples; spike lasts only 5 minutes (300s of 3600s).
TOTAL = 1_800_000
SPIKE_FRAC = 5/60 # 8.3% of the hour
n_spike = int(TOTAL * SPIKE_FRAC)
n_normal = TOTAL - n_spike
def draw(profile: dict, n: int) -> dict:
fns = list(profile.keys())
p = np.array(list(profile.values()))
drawn = rng.choice(len(fns), size=n, p=p)
counts = np.bincount(drawn, minlength=len(fns))
return dict(zip(fns, counts / n))
# What the 1-hour aggregate sees
combined_counts = {}
for fn, share in regime_normal().items():
combined_counts[fn] = combined_counts.get(fn, 0) + share * n_normal
for fn, share in regime_spike().items():
combined_counts[fn] = combined_counts.get(fn, 0) + share * n_spike
hour_view = {fn: c / TOTAL for fn, c in combined_counts.items()}
# What the 5-minute focused query would see
spike_view = draw(regime_spike(), n_spike)
print(f"1-hour aggregate (averages over the spike):")
for fn, share in sorted(hour_view.items(), key=lambda x: -x[1]):
print(f" {fn:25s} {share:.3f}")
print(f"\n5-minute spike-window aggregate (the spike isolated):")
for fn, share in sorted(spike_view.items(), key=lambda x: -x[1]):
print(f" {fn:25s} {share:.3f}")
print(f"\njson_parse hour-view: {hour_view['json_parse']:.3f}")
print(f"json_parse spike-view: {spike_view['json_parse']:.3f}")
print(f"the spike is invisible in the hour view — this is why bucket size matters")
Sample run:
1-hour aggregate (averages over the spike):
verify_signature 0.375
everything_else 0.250
json_parse 0.229
db_query 0.146
5-minute spike-window aggregate (the spike isolated):
json_parse 0.551
everything_else 0.249
verify_signature 0.102
db_query 0.098
json_parse hour-view: 0.229
json_parse spike-view: 0.551
the spike is invisible in the hour view — this is why bucket size matters
SPIKE_FRAC = 5/60 — the spike is 5 minutes of a 60-minute window, 8.3% of the time. The hour-view averages the spike's 0.55 share with the normal 0.20 share, giving 0.23 — barely above normal noise. The fleet profiler's UI shows nothing alarming.
spike_view['json_parse']: 0.551 — querying the 5-minute window directly reveals the spike clearly. The signal is there in the data; the question is whether the query layer lets you ask for it. GWP's freshness contract — fine-grained buckets for fresh data — is what makes this query possible. A profiler that only stores hour-aggregates throws the spike away forever.
Why this matters at Indian-fintech scale: the most-painful production incidents (UPI-batch-window spikes at the 1st of the month, IPL-final viewer surges, IRCTC Tatkal at 10:00 IST sharp) are precisely the ones that show up as 5-15 minute regime changes inside a longer day. A continuous profiler that only retains 1-hour aggregates is useless for these incidents — and most homegrown profilers do exactly this because hourly aggregation is cheap. The GWP paper's freshness contract was the architectural decision that made spike-debugging possible, and skipping it is the most common way teams accidentally render their profiler useless for the cases they need it most.
The paper also calls out the regime where this breaks: non-stationary workloads. If your service's CPU profile shifts during the IPL final from 21:00 to 22:00 IST, a 6-hour aggregate that spans 18:00–24:00 averages over the regime change, hiding the spike that the on-call cares about. GWP's response was the freshness contract — short time buckets (5 min) for fresh data, larger buckets (1 hour, 1 day) for older — so an SRE could query "the 22:00 IST bucket only" and see the spike's profile in isolation. This is exactly what Aditi did at the top of the chapter.
What the open-source descendants kept and what they dropped
GWP was internal. Its design choices became open-source through a chain of papers, blog posts, and ex-Google engineers founding profiler companies. Mapping which choices survived is how you understand the modern landscape.
What the open-source descendants kept: the build-id-keyed sample (constraint #2), the late-symbolisation discipline (constraint #2), the storage-as-aggregation insight (constraint #3 — though Pyroscope and Parca differ on which aggregation), and the under-1% per-machine overhead floor (constraint #1). What got dropped: the single-tenant fleet model (replaced with multi-tenant ingestion paths), the Bigtable storage shape (replaced with object-store blocks or columnar Parquet), the centralised build-farm-driven symbolisation (replaced with debuginfod HTTP endpoints), the per-cluster cron schedule (replaced with Prometheus-style scrape or per-process push), and the SQL query layer (replaced with FlameQL or PromQL-shaped queries).
Two of those drops introduced new failure modes that the paper had quietly avoided. The first is multi-tenancy: GWP at Google was one fleet, one schema; modern profilers ingest from many tenants with different label schemas, and the cardinality blow-ups that happen at Razorpay scale (tenant × service × pod × build_id × function) are a problem GWP never had to solve. The second is decentralised symbolisation: a debuginfod server returning a symbol map for an unknown build-id over HTTP is fundamentally slower and less reliable than a tightly-coupled build farm. When a Hotstar engineer sees 0x7f8a2c3b4567 instead of a function name in their flamegraph, the cause is almost always a debuginfod miss — a problem GWP solved by construction in 2005 and the open-source ecosystem keeps re-solving piecemeal.
Common confusions
- "GWP is the same as Linux
perf" — incorrect.perfis a per-process, per-session sampler with no fleet aggregator, no build-id-keyed dedup, no central storage, and no query layer. GWP usesperf-style kernel sampling at the lowest layer (constraint #1 surface), but the value of the system is in stages 2–4 of the pipeline. A team that runsperf recordon every machine and ships the output to S3 has built one stage of GWP, not GWP. - "GWP samples every request" — false. GWP samples CPU cycles, not requests. A request that runs for 200ms on one CPU has at most 2 samples in it (at 100Hz). Per-request profiling is a different problem (per-trace flamegraphs, often called "request profiling" or "Pixie-style"); GWP is fleet-level statistical attribution, which is why the per-machine rate can be so low.
- "The GWP paper proves you need 1M machines for fleet profiling" — wrong direction. The paper proves the constraints scale with fleet size, but the same statistical argument works at 100 machines (you'd want a higher per-machine rate, ~1 sample per CPU per 1s, to maintain attribution accuracy). Razorpay at 1,400 pods runs Pyroscope at 100Hz per pod and gets accurate per-function attribution within minutes; the math is unchanged.
- "Continuous profiling and continuous tracing are the same thing" — different. A continuous tracer (Tempo, Jaeger) keeps a sampled subset of request traces with span hierarchies. A continuous profiler keeps sampled CPU stacks aggregated by function. A trace tells you "which spans took how long in this request"; a profile tells you "which functions consumed CPU across all requests in the last hour". Use a trace to debug a slow request; a profile to debug a slow service. The sub-discipline that fuses them is span profiling (chapter 60-ish in this curriculum).
- "Sampling at 0.01% misses rare bugs" — true at the per-request level, false at the per-function-CPU level. A bug that happens in 1-in-10000 requests will not appear in your 0.01%-sampled traces, but if it consumes any meaningful share of fleet-wide CPU (because all 10000 requests still share the same hot function), it absolutely shows up in GWP-style attribution. The paper's argument is precisely that fleet-level statistical attribution and request-level diagnostic sampling are different problems.
- "GWP is a flamegraph tool" — flamegraphs (Brendan Gregg, 2011) post-date the GWP paper by a year. GWP rendered ranked function lists and call-graph diffs in 2010; flamegraphs were the visualisation that made the underlying data legible to non-experts. GWP-the-system is the data pipeline; flamegraphs are how the modern tools display its descendants' output.
Going deeper
The "two attribution biases" the paper flags but does not fully solve
§5 of the paper enumerates two known biases that fleet sampling cannot eliminate cheanly. The first is kernel-mode under-attribution: most kernel-side time (page-faults, futex wait, scheduler) is not visible to user-space samplers, so the flamegraph's "everything_else" bucket is artificially large for I/O-bound services. GWP's response was to instrument kernel perf_events separately and expose them as a parallel ranked list, but the paper admits this was not a complete fix. The eBPF descendants (Parca-Agent's kernel-stack support, Pixie) do better by walking kernel and user stacks in the same bpf_get_stack call, but the bias is still real for syscalls that block in deep kernel paths. The second bias is JIT mis-attribution: a JVM that re-JITs a hot method after observing it cannot retroactively re-symbolise samples taken pre-JIT, so the flamegraph shows the same source line under two different function names. GWP punted on this; modern profilers use JVMTI hooks to dump symbol maps that align across JIT boundaries.
Why the storage choice (Bigtable, schema-per-row) was the right one for 2005
In 2005, when GWP launched, Bigtable was the only storage system at Google that scaled to the fleet's sample volume (paper estimates ~5 PB raw, compressed to ~500 GB after aggregation). The schema choice — one row per (time_bucket, binary, build_id, function, line) with sample_count as the cell value — exploits Bigtable's strength: O(1) lookup by row key, O(scan-length) range queries, no secondary indexes. A query for "show me the top 10 functions in payments-api build ab123 over the last hour" was a range-scan over a tiny prefix of the table; a query for "diff version A and version B" was two range-scans subtracted. The modern columnar engines (FrostDB, the ones backing ClickHouse-flavoured profilers) achieve better per-byte compression and better column-pruning, but the fundamental access pattern — row-keyed by (time, binary, function) — is the same. The paper's storage chapter is the most-skipped section of the paper and the one that most influenced 15 years of profiler storage design.
The "build-farm symbolisation contract" that nobody outside Google has
GWP's most underrated decision was that the build farm — Google's internal Bazel-derived build system — wrote debuginfo artefacts for every binary it produced and made them queryable by build-id forever. This meant that when a sample arrived with build_id=ab12cd34, the central aggregator could fetch the exact symbol map for that exact binary version, even if the source had since moved. No DWARF unwinding, no .symtab heuristics, no version-skew bugs. The open-source equivalent is debuginfod (elfutils 2019), but its adoption is patchy: most Indian fintech build pipelines do not produce debuginfo artefacts, do not push them to a debuginfod mirror, and do not retain them past the binary's deploy lifetime. So when a Razorpay engineer profiles a stripped production binary built six months ago and sees raw addresses, they are paying the cost of not having Google's build-farm contract. The fix is mechanical (run a debuginfod server, configure CI to push debuginfo artefacts), but it requires platform-team buy-in that profile-team alone cannot deliver.
What the paper got wrong (in retrospect)
Two predictions in the paper aged badly. The first: §6 implies that fleet profiling is a batch discipline, with profile lifetimes measured in hours-to-days. Modern continuous profilers (Pyroscope, Parca) ship sub-minute profile freshness, and the on-call workflow at Hotstar/Razorpay assumes a profile is ready within 90 seconds of a workload shift. The paper's batch assumption was correct for 2005 datacentres but wrong for 2025 cloud-native shops where deploys happen every 11 minutes. The second: the paper assumes a single language and a single binary format (Google was almost entirely C++ in 2010). Modern fleet profilers must handle Python interpreter frames, JVM JIT, Go runtime stacks, Node.js V8, .NET CLR — and each requires bespoke unwinder code that the paper's "build-id → symbol" contract does not generalise to. This is why Parca-Agent's release history is a trail of language-specific unwinders shipped one after another, and why Pyroscope's SDK approach (one per language) was a defensible alternative bet.
How to actually read the paper for an Indian-fintech audience
Skip §1 and §2 (motivation, related work — read after the rest, they make more sense in retrospect). Read §3 (architecture) twice — the first read for shape, the second with a continuous profiler's docs open in another tab, to map each GWP component to its modern equivalent. Read §4 (sampling and overhead) carefully — the math is the chapter; ignore the specific machine counts. Read §5 (biases) in full — these are the bugs you will rediscover. Read §6 (results) skimmingly — the case studies are interesting but dated. The whole reading takes ~90 minutes. Pair with parca-pixie-pyroscope (this curriculum's chapter 53) for a side-by-side mapping of paper concepts to modern tools.
Where this leads next
Chapter 58 — eBPF profiling internals — picks up the technical thread of how the paper's "100ms perf-style snapshot" became perf_event_open plus bpf_get_stack, including the difference between frame-pointer unwinding (cheap) and DWARF unwinding (expensive) and why the open-source profilers each pick a different point on that trade-off. Chapter 59 — pprof format and its successors — dissects the protobuf format that grew out of GWP's internal binary format and is now the lingua franca of every continuous profiler.
For deeper paper reading, /wiki/the-dapper-paper-2010 covers Dapper, GWP's distributed-tracing sibling — same Google research culture, same year, very different problem (request-level instead of fleet-level). Together they sketch the observability stack Google built before "observability" was a category.
For the Indian-engineer pragmatic path, /wiki/pyroscope-and-parca-architectures (the previous chapter) lets you compare two GWP-descendant systems and pick which one your team can actually run, given which upstream systems you already operate.
References
- Ren, Tene, Sites, Kim, Tashev, Hundt — Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers (IEEE Micro, July/August 2010) — the paper itself; 12 pages, foundational.
- Brendan Gregg — Systems Performance: Enterprise and the Cloud (2nd ed., Pearson, 2020) — chapter 6 (CPUs) frames continuous profiling within the broader perf-engineering discipline.
- Sigelman et al. — Dapper, a Large-Scale Distributed Systems Tracing Infrastructure (Google Tech Report, 2010) — GWP's tracing sibling; same year, same culture.
- Google: pprof profile format spec — the protobuf descendant of GWP's internal sample format.
- elfutils debuginfod documentation — the open-source equivalent of GWP's build-farm symbolisation contract.
- Cindy Sridharan — Distributed Systems Observability (O'Reilly, 2018) — chapter 4 frames profiling as the "fourth pillar".
- /wiki/pyroscope-and-parca-architectures — chapter 56 of this curriculum, on the open-source descendants.
- /wiki/the-dapper-paper-2010 — the tracing-side companion paper from the same era.
# Reproduce this on your laptop
python3 -m venv .venv && source .venv/bin/activate
pip install numpy pandas
python3 gwp_attribution_error.py