Skip to content

Commit 17c09ad

Browse files
authored
Merge branch 'main' into post-2d-multiplayer
2 parents 2666173 + 481576a commit 17c09ad

File tree

10 files changed

+677
-12
lines changed

10 files changed

+677
-12
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ Issues/bug reports are very welcome.
99
## Features
1010

1111
- Next.js hosted on Vercel
12-
- Markdown + images
12+
- Markdown, images, and videos
1313
- RSS feed (links, not full content)
1414
- Simple design focused on content (responsive for desktop/mobile)
15-
- Code highlighting via `prism-react-renderer`
15+
- Code highlighting via `prism-react-renderer` (TODO fix Lisp syntax highlighting)
1616
- Newsletter CTA (powered by Buttondown)
1717
- All core features work without JavaScript enabled
1818
- End-to-end tests with `playwright`
@@ -40,4 +40,4 @@ npm run dev
4040

4141
Feel free to use any of my writing or code for educational reasons (e.g. you're teaching a class).
4242

43-
Otherwise, check with me before republishing my writing or code (I'll give permission 99% of the time).
43+
Otherwise, check with me before republishing my writing (I'll give permission 99% of the time).

data/posts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const popularPosts = [
88
// Good posts/highly viewed posts (not in any specific order)
99
export const postStars = [
1010
"2d-multiplayer-from-scratch",
11+
"lisp-compiler-optimizations",
12+
"lisp-to-javascript-compiler",
1113
"compressing-cs2-demos",
1214
"a-custom-webassembly-compiler",
1315
"rendering-counter-strike-demos-in-the-browser",

data/projects.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default [
88
{
99
name: "nodots",
1010
link: "https://github.com/healeycodes/nodots-lang",
11-
desc: "A small programming language with an interpreter, and a WebAssembly compiler.",
11+
desc: "A small programming language with an interpreter, a profiler, and a WebAssembly compiler.",
1212
to: "/a-custom-webassembly-compiler",
1313
},
1414
{
@@ -17,6 +17,12 @@ export default [
1717
desc: "A text editor for macOS. Built using the Ebitengine game engine.",
1818
to: "/making-a-text-editor-with-a-game-engine",
1919
},
20+
{
21+
name: "lisp-to-js",
22+
link: "https://github.com/healeycodes/lisp-to-js",
23+
desc: "A Lisp-to-JavaScript optimizing compiler written in Rust. Supports a variant of Little Lisp.",
24+
to: "/lisp-to-javascript-compiler",
25+
},
2026
{
2127
name: "jar",
2228
link: "https://github.com/healeycodes/jar",

pages/about.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ export default function About() {
1111
<h1>About</h1>
1212
<main>
1313
<p>
14-
I write software and write about software. I{" "}
15-
<a href="mailto:[email protected]">love getting email</a>. My research interests include programming languages, game solvers (chess, sokoban, and more), and isolation/sandboxing.
16-
</p>
14+
I write software and write about software. My research interests include programming language design, compilers, JavaScript runtimes, game solvers, and running untrusted code.</p>
1715
<p>
18-
This <a href={siteConfig.REPO_URL}>open source</a> website is built with Next.js.
16+
This <a href={siteConfig.REPO_URL}>open source</a> website is built with Next.js, and I <a href="mailto:[email protected]">love getting email.</a>
1917
</p>
2018
<SpacedImage
2119
src={mePresenting}
@@ -28,8 +26,7 @@ export default function About() {
2826
style={{ borderRadius: '0.4em' }}
2927
/>
3028
<p>
31-
I like teaching people things that I know. I like video games,
32-
running, and reading.
29+
I like teaching people things that I know. I like video games, classic games (chess, scrabble, sudoku), running, and reading.
3330
</p>
3431
<p>
3532
I am easily impressed by people and the cool stuff they build. I

pages/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default function Home({ allPostsData, description, words }) {
5555
style={{ borderRadius: '0.4em' }}
5656
/>
5757
<p className="avatar-text">
58-
Hey, I'm Andrew Healey. I'm a software engineer at Vercel, and I'm interested in the joy of computing. I've written{" "}
58+
Hey, I'm Andrew Healey. I'm a software engineer at Vercel, and I'm interested in the <Link href="/my-time-at-the-recurse-center">joy of computing</Link>. I've written{" "}
5959
{numberWithCommas(words)} words on this{" "}
6060
<a href={siteConfig.REPO_URL}>open source</a> website.
6161
</p>

pages/projects.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function Projects({ totalStars, mostRecentPushFormatted }: { tota
1717
<h1>Projects</h1>
1818
<main>
1919
<p>
20-
My side projects include programming languages, web frameworks, game solvers, developer tools, databases, and games. My public GitHub repositories have been starred {totalStars} times, and my last open source <code>git push</code> was {mostRecentPushFormatted} ago.
20+
My side projects include interpreters and compilers, web frameworks, game solvers, developer tools, databases, games, and more. My public GitHub repositories have been starred {totalStars} times, and my last open source <code>git push</code> was {mostRecentPushFormatted} ago.
2121
</p>
2222
<h2>Open Source</h2>
2323
<div className="project-list">
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
title: "Adding a Line Profiler to My Language"
3+
date: "2024-03-26"
4+
tags: ["python"]
5+
description: "Creating my own developer tooling, and some thoughts on line profilers."
6+
---
7+
8+
When I worked on [profiling and optimizing the interpreter](https://healeycodes.com/profiling-and-optimizing-an-interpreter) for my toy programming language [nodots](https://github.com/healeycodes/nodots-lang), I was missing a tool to help me answer the question: how long does a line of nodots source code take to run? Such a tool would help me measure the impact of my performance improvements, and also help me write faster nodots programs.
9+
10+
When I saw CanadaHonk post a [screenshot](https://twitter.com/CanadaHonk/status/1769502527391744346) of their in-terminal profiler, I remembered this lost idea and started hacking on a feature to track the performance of lines in nodots.
11+
12+
I [added](https://github.com/healeycodes/nodots-lang/pull/5) a new flag to the CLI, `--profile`, that prints performance statistics right next to the program's source code. I implemented my favorite statistics from Python's [cProfile](https://docs.python.org/3/library/profile.html) module; number of calls, the total time of those calls, and how long each call took.
13+
14+
```text
15+
$ python cli.py --profile fib.nd
16+
ncalls tottime percall
17+
for (i = 0; i < 21; i = i + 1)
18+
# recursive (slow)
19+
fun fib(x)
20+
if (x == 0 or x == 1)
21+
return x;
22+
fi
23+
return fib(x - 1) + fib(x - 2); x57270 11.2s 195µs
24+
nuf
25+
log(fib(i)); x42 1.9s 46ms
26+
rof
27+
```
28+
29+
Getting immediate feedback right in my terminal has improved my iteration velocity when testing performance changes to my interpreter. Even with the reduced granularity compared to a more traditional profiling tool, I quite like the combined call statistics in this compact user interface.
30+
31+
## Line Profiler Internals
32+
33+
When the nodots interpreter is executing code, it's evaluating a tree of tokens. Examples of tokens are things like numbers, strings, variables, or function calls. Each token knows its line and column.
34+
35+
Initially, I tried to track the duration of *everything* on *every* line but the output was a noisy sea of numbers. So I decided to just track function calls for now (most performance profiles track function calls because functions are the basic building blocks of programs and usually represent significant work).
36+
37+
When the profiling flag is enabled, a tracking function gets passed a line and a duration every time a nodots function is called.
38+
39+
```python
40+
def track_call(self, line: int, duration: float):
41+
if self.profile:
42+
self.line_durations["calls"].append((line, duration))
43+
```
44+
45+
The duration of each call is measured inside `eval_call`.
46+
47+
```python
48+
def eval_call(node: Tree | Token, context: Context) -> Value:
49+
# ...
50+
51+
# measure calls
52+
start = time.perf_counter()
53+
current_func = current_func.call_as_func(
54+
node.children[0].meta.line,
55+
node.children[0].meta.column,
56+
eval_arguments(args, context) if args else [],
57+
) # call duration
58+
context.track_call(node.children[0].meta.line, time.perf_counter() - start)
59+
```
60+
61+
After a nodots program's execution completes, the durations are converted into line statistics.
62+
63+
```python
64+
def print_line_profile(self, source: str):
65+
# ...
66+
67+
# convert raw durations into statistics
68+
line_info: Dict[int, List[str]] = {}
69+
for ln, line in enumerate(source.splitlines()):
70+
line_info[ln] = [
71+
# ncalls
72+
f"x{len(line_durs[ln])}",
73+
# tottime
74+
f"{format_number(sum(line_durs[ln]))}",
75+
# percall
76+
f"{format_number((sum(line_durs[ln]) / len(line_durs[ln])))}",
77+
]
78+
```
79+
80+
I also formatted the statistics into human-friendly units:
81+
82+
```python
83+
def format_number(seconds: float) -> str:
84+
if seconds >= 1:
85+
return f"{round(seconds, 1)}s"
86+
elif seconds >= 0.001:
87+
return f"{int(seconds * 1000)}ms"
88+
return f"{int(seconds * 1000 * 1000)}µs"
89+
```
90+
91+
92+
The majority of my effort on this feature probably went into displaying the data rather than creating it. The hardest challenge was lining everything up in rows and columns with the correct offset from the source code.
93+
94+
The overhead of all this tracking and processing takes ~130ms of the ~1125ms total in the example at the top of this post.
95+
96+
The current profiling implementation needs quite a lot of memory. Inefficiently, it stores one tuple for every call. The optimal amount of space is bounded to the number of lines of source code (as opposed to the number of calls made during a program's execution). Each line needs to know it's *number of calls* and the *total time taken* — and then statistics can be derived from this data.
97+
98+
## Should More Tooling Support Line-Specific Measurements?
99+
100+
No. Line profiling is quite a zoomed-in perspective. Usually, practitioners are more interested in the per-request or per-function level of performance. And then, if they need more granular information, they'll manually inspect the function, or isolate it and run benchmarks.
101+
102+
Collecting and rolling up line profile data on a wide-scale also introduces complexity for the provider and consumer of this data. Line profiling makes it near-impossible to track performance over time. When you make a code change, your existing data gets voided. Ideally, you can make a change to a function, deploy your code change, and then query your data to see how that function's performance has changed.
103+
104+
But I do find myself wishing that the mainstream programming languages I use had a tool like my line profiler here as it makes it quicker to create and consume performance profiles of small sections of code.

0 commit comments

Comments
 (0)