Skip to content

Commit 5e2c77f

Browse files
committed
feat: Add files for profiling blogpost
1 parent 724fe63 commit 5e2c77f

20 files changed

Lines changed: 177 additions & 20 deletions

_posts/2025-11-25-a-tale-of-two-kernels.markdown

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ date: 2025-11-25 20:36:38 +0100
55
categories: blog
66
---
77

8-
### Overview
9-
108
For developers integrating Haskell into data science workflows or interactive documentation, the Jupyter notebook is the standard interface. Currently, there are two primary ways to run Haskell in Jupyter: [IHaskell](https://github.com/IHaskell/IHaskell) and [xeus-haskell](https://github.com/jupyter-xeus/xeus-haskell).
119

1210
While both achieve the same end user experience (executing Haskell code in cells) their internal architectures represent fundamentally different engineering trade-offs:
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
layout: post
3+
title: "Exploring GHC profiling data in Jupyter"
4+
date: 2025-12-26 14:01:38 +0100
5+
categories: blog
6+
---
7+
8+
9+
Exploratory data analysis (EDA) isn't just for data scientists. Anyone that uses a system that emits data can benefit from the tools of EDA. And since charity begins at home, what better way to motivate this than a short post using DataHaskell tools to analyse GHC profiling logs.
10+
11+
By treating performance analysis as a data exploration problem, we may unlock insights that might be difficult to see in a static report.
12+
13+
This article is for you if:
14+
15+
* • You've profiled Haskell programs before and want a more flexible way to explore the data beyond what static tools provide: filtering, joining, and comparing runs programmatically.
16+
* • You're comfortable with data tools (pandas, R, SQL) but haven't worked with GHC's profiling infrastructure this will show you how to bridge that gap.
17+
* • You want to compare performance across code changes by diffing two profiling runs to see exactly what got better or worse, attributed to specific cost centres.
18+
* • Or you're just curious about DataHaskell!
19+
20+
## The example program
21+
22+
For our scenario today we're going to compare the memory behaviour of two variants of a simple summing function. The first is a textbook strict fold:
23+
24+
```haskell
25+
sumFast :: B.ByteString -> Int
26+
sumFast bs =
27+
let xs = parseAll bs
28+
in foldlStrict (\ !acc x -> acc + x) 0 xs
29+
```
30+
31+
The second version is almost identical, but it retains a sample of partial sums as it runs—a controlled memory leak that accumulates data over time:
32+
33+
```haskell
34+
data Acc = Acc !Int !Int !SampleList
35+
36+
sumLeaky :: B.ByteString -> Int
37+
sumLeaky bs =
38+
let xs = parseAll bs
39+
Acc s _ samples = foldlStrict step (Acc 0 0 Nil) xs
40+
in sampleLength samples `seq` s
41+
where
42+
step (Acc s i history) x =
43+
let !s' = s + x
44+
!i' = i + 1
45+
!history' = if i' `rem` 50000 == 0
46+
then Cons s' history
47+
else history
48+
in Acc s' i' history'
49+
```
50+
51+
Both compute the same result. Both have similar runtime. But one retains memory that the other doesn't—and we want to see that difference in the profiling data.
52+
53+
To generate eventlogs, you need to:
54+
55+
1. Compile with profiling enabled: `cabal build --enable-profiling --profiling-detail=all-functions -O2`
56+
2. Run with the right RTS flags: `cabal run your-program -- +RTS -hc -l-agu -RTS`
57+
58+
Here `-hc` requests heap profiling by cost centre (we'll explain what that means shortly), and `-l-agu` enables the eventlog while disabling some less relevant event types. This produces a `.eventlog` file alongside your executable. For our example, we generate two: `fast.eventlog` and `leaky.eventlog`.
59+
60+
The standard tool for visualising these logs is eventlog2html, which produces beautiful interactive HTML reports.
61+
62+
![A heap usage graph generated by eventlog2html](/images/heap_usage_eventlog2html.png)
63+
![A cost center usage graph generated by eventlog2html](/images/cost_center_breakdown_eventlog2html.png)
64+
65+
It's excellent for getting a quick overview—but sometimes you need more. You might want to filter to specific cost centres, compare two runs side-by-side, compute derived metrics, or ask questions the tool's authors didn't anticipate.
66+
67+
We'd like to see if we can get this and then more using a SQL-like API to explore our eventlog data. In this blog post we'll do two things: firstly, we'll show how to regenerate the heap usage chart from eventlog2html. Secondly we'll show how to use operations like join to diff two runs.
68+
69+
## Generating eventlogs
70+
71+
GHC can emit detailed runtime information into eventlog files. These binary logs contain a stream of timestamped events: garbage collection statistics, heap samples, cost centre attributions, and more.
72+
73+
## From eventlogs to dataframes
74+
75+
An eventlog is just a pile of raw facts with disjointed descriptions of what happened and when it happened. To turn it into answers, we need tools that can ingest event streams, normalise timestamps, join disparate metrics, and visualise relationships.
76+
77+
We'll use the ghc-events library to parse the binary format, then load the data into a DataFrame. This gives us a familiar columnar interface. If you've used pandas or dplyr, the operations should feel natural.
78+
79+
We write some [custom code to parse eventlog files](https://gist.github.com/mchav/ae93567450075fb65d0579254f7dc406) into structures that we'll eventually turn into dataframes.
80+
81+
![Reading eventlogs into a heap profile dataframe](/images/read_into_dataframe.png)
82+
83+
After parsing, we have a table with three columns: time, cc_label (the cost centre label), and residency (bytes retained):
84+
85+
![Sampling the heap dataframe](/images/sample_heapdf.png)
86+
87+
### A quick note on cost centres
88+
89+
A cost centre is GHC's unit of attribution for profiling. When you compile with `-profiling-detail=all-functions`, GHC inserts cost centres at function boundaries. Each heap sample then records how much memory is attributed to each cost centre.
90+
Cost centre labels look like `sumLeaky.step.history' (Main)`. That's the function history' defined inside step inside sumLeaky, in the Main module. These hierarchical names let you trace allocations back to specific expressions in your code.
91+
92+
## Aggregation
93+
94+
Raw performance data is noisy. The runtime might emit a "Heap Size" event every few microseconds, creating thousands of data points per second of runtime. Using DataFrame functions, we can group these by a coarser time grain and calculate the mean, effectively "smoothing" the signal.
95+
96+
![Aggregate heap by cost center](/images/aggregate_residency.png)
97+
98+
![A sample of the aggregated residency dataframe](/images/sample_aggregate_residency.png)
99+
100+
Now we have one row per cost centre, with summary statistics. But the real power comes when we compare runs.
101+
102+
## Joining two profiling runs
103+
104+
To diff the fast and leaky versions, we perform a full outer join on the cost centre label:
105+
106+
![A diff of memory usage between runs](/images/diff_residency_of_runs.png)
107+
108+
The result tells us exactly where the memory went. The history' binding where we cons onto the sample list accounts for the retained memory.
109+
110+
## Correlating different metrics
111+
112+
Eventlogs contain multiple types of events. Beyond heap samples by cost centre, we get:
113+
114+
* • HeapLive: actual live bytes after GC
115+
* • HeapSize: total heap size requested from the OS
116+
* • BlocksSize: memory in block allocator
117+
118+
We can parse these into separate DataFrames, aggregate by time, and join them together:
119+
120+
![Join the different memory usages](/images/join_heap_bytes.png)
121+
122+
This gives us a wide table where each row is a time bucket, and columns show different metrics for each run:
123+
124+
![A sample of the joined heap bytes](/images/sample_join_heap_bytes.png)
125+
126+
## Plotting the results
127+
128+
Now we can go ahead and plot the famous eventlog2html heap usage chart with both runs in the same chart.
129+
130+
![A recreation of the eventlog2html plot but with two runs](/images/full_heap_chart.png)
131+
132+
We can "zoom in" on the chart by taking the first 5 rows and see the difference in live bytes.
133+
134+
![A chart showing just the first few seconds of the run](/images/zoomed_in_chart.png)
135+
136+
The leaky version shows steadily increasing live bytes; the fast version stays flat. But more importantly, we can now query this data: what's the rate of growth? When does the leak become significant? Does block size track live bytes, or does the allocator hold onto memory after GC reclaims it?
137+
138+
As an added bonus, We can run our profiling code in GHCi/a regular script as well and produce similar output.
139+
140+
![A profiling chart in the terminal](/images/terminal_run.png)
141+
142+
## What we learned
143+
144+
The notebook captures the entire pipeline from raw eventlog to final visualisation. Re-run it after a code change and you get an updated diff automatically. Because everything is in DataFrames, we can filter to specific time ranges, compute ratios, correlate with GC events, or export to CSV for further analysis in other tools.
145+
146+
This gives us a flexible way of exploring performance.
147+
148+
The full notebook is available to explore in the [DataHaskell playground](https://ulwazi-exh9dbh2exbzgbc9.westus-01.azurewebsites.net/lab/tree/examples/performance_exploration.ipynb).
149+
150+
## What's next?
151+
152+
We are currently working on turning this workflow into a dedicated library. We're working with developers to see what data and charts will be the most useful for understanding performance. Our goal is to provide a seamless bridge between the GHC RTS and high-level analysis tools. Along with the library, we will be providing guides on how to use these data-science techniques to diagnose specific performance pathologies.
153+
154+
As always watch this space and if this sort of work is interesting to you, hop over to the DataHaskell discord to get in on the action.

_sass/_layout.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
.site-title {
1414
font-size: 26px;
15-
font-weight: 300;
15+
font-weight: 400;
1616
line-height: 56px;
1717
letter-spacing: -1px;
1818
margin-bottom: 0;
@@ -215,6 +215,7 @@
215215

216216
.post-content {
217217
margin-bottom: $spacing-unit;
218+
padding: 1em;
218219

219220
h2 {
220221
font-size: 32px;

_sass/_normalize.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ abbr[title] {
114114

115115
b,
116116
strong {
117-
font-weight: bold;
117+
font-weight: bolder;
118118
}
119119

120120
/**
@@ -404,7 +404,7 @@ textarea {
404404
*/
405405

406406
optgroup {
407-
font-weight: bold;
407+
font-weight: bolder;
408408
}
409409

410410
/* Tables

assets/css/main.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,7 @@
15691569
color: black;
15701570
font-family: 'Lato', sans-serif;
15711571
font-size: 15pt;
1572-
font-weight: 300;
1572+
font-weight: 400;
15731573
letter-spacing: 0.025em;
15741574
line-height: 1.75em;
15751575
}
@@ -1589,7 +1589,7 @@
15891589
}
15901590

15911591
strong, b {
1592-
font-weight: 400;
1592+
font-weight: 900;
15931593
}
15941594

15951595
p, ul, ol, dl, table, blockquote {
@@ -1598,7 +1598,7 @@
15981598

15991599
h1, h2, h3, h4, h5, h6 {
16001600
color: inherit;
1601-
font-weight: 300;
1601+
font-weight: 900;
16021602
line-height: 1.75em;
16031603
margin-bottom: 1em;
16041604
text-transform: uppercase;
@@ -2202,7 +2202,7 @@
22022202
}
22032203

22042204
#header h1 span {
2205-
font-weight: 300;
2205+
font-weight: 900;
22062206
}
22072207

22082208
#header nav {

assets/sass/main.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
color: _palette(fg);
7171
font-family: 'Lato', sans-serif;
7272
font-size: 15pt;
73-
font-weight: 300;
73+
font-weight: 400;
7474
letter-spacing: 0.025em;
7575
line-height: 1.75em;
7676
}
@@ -96,7 +96,7 @@
9696

9797
h1, h2, h3, h4, h5, h6 {
9898
color: inherit;
99-
font-weight: 300;
99+
font-weight: 400;
100100
line-height: 1.75em;
101101
margin-bottom: 1em;
102102
text-transform: uppercase;
@@ -503,7 +503,7 @@
503503
}
504504

505505
th {
506-
font-weight: 400;
506+
font-weight: 500;
507507
padding: 0.5em 1em 0.5em 1em;
508508
text-align: left;
509509
}

blog.html

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@
66

77
<div class="posts">
88
{% for post in site.posts %}
9-
<article class="post">
9+
<div style="padding: 1em; margin: 1em; border-style: solid; border-radius: 1em; border-color: grey; border-width: 0.1em;">
10+
<hr>
11+
<article class="post">
1012

11-
<h1><a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a></h1>
13+
<h1><a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a></h1>
1214

13-
<div class="entry">
14-
{{ post.excerpt }}
15-
</div>
15+
<div class="entry">
16+
{{ post.excerpt }}
17+
</div>
1618

17-
<a href="{{ site.baseurl }}{{ post.url }}" class="read-more">Read More</a>
18-
</article>
19+
<a href="{{ site.baseurl }}{{ post.url }}" class="read-more">Read More</a>
20+
</article>
21+
<hr>
22+
</div>
1923
{% endfor %}
2024
</div>
2125

css/main.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Our variables
99
$base-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
1010
$base-font-size: 16px;
11-
$base-font-weight: 400;
11+
$base-font-weight: 500;
1212
$small-font-size: $base-font-size * 0.875;
1313
$base-line-height: 1.5;
1414

images/aggregate_residency.png

71.3 KB
Loading
41.1 KB
Loading

0 commit comments

Comments
 (0)