Skip to content

Commit 256e3c3

Browse files
authored
Merge pull request #289 from plasma-umass/swift-bindings
Add Swift bindings for coz
2 parents 81266e1 + 299ffc1 commit 256e3c3

9 files changed

Lines changed: 794 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,58 @@ jobs:
482482
483483
- name: Validate the Java profile
484484
run: python3 .github/scripts/check_profile.py profile.jsonl Toy.java
485+
486+
lang-swift:
487+
name: Swift bindings (${{ matrix.os }})
488+
runs-on: ${{ matrix.os }}
489+
strategy:
490+
fail-fast: false
491+
matrix:
492+
os: [ubuntu-latest, macos-latest]
493+
494+
steps:
495+
- uses: actions/checkout@v4
496+
497+
- name: Install dependencies (Linux)
498+
if: runner.os == 'Linux'
499+
run: |
500+
sudo apt-get update
501+
sudo apt-get install -y build-essential cmake pkg-config
502+
echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid
503+
504+
- name: Install Swift (Linux)
505+
if: runner.os == 'Linux'
506+
uses: swift-actions/setup-swift@v2
507+
with:
508+
swift-version: '6.1'
509+
510+
- name: Install dependencies (macOS)
511+
if: runner.os == 'macOS'
512+
run: brew install pkg-config coreutils || true
513+
514+
- name: Build coz
515+
run: |
516+
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
517+
cmake --build build -j3
518+
test -f build/libcoz/libcoz.so || test -f build/libcoz/libcoz.dylib
519+
520+
- name: Test the package
521+
working-directory: swift
522+
run: |
523+
swift test
524+
# The library must stay clean under Swift 6 strict concurrency.
525+
swift build -Xswiftc -swift-version -Xswiftc 6
526+
527+
- name: Build the Swift toy
528+
working-directory: swift
529+
run: swift build -c release -Xswiftc -g
530+
531+
- name: Profile the Swift toy under coz
532+
run: |
533+
TIMEOUT=timeout
534+
command -v gtimeout >/dev/null && TIMEOUT=gtimeout
535+
$TIMEOUT 180 ./coz run -o profile.jsonl --- ./swift/.build/release/CozToy || true
536+
test -s profile.jsonl
537+
538+
- name: Validate the Swift profile
539+
run: python3 .github/scripts/check_profile.py profile.jsonl main.swift

swift/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.build/

swift/Package.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// swift-tools-version:5.9
2+
/*
3+
* Copyright (c) 2015, Charlie Curtsinger and Emery Berger,
4+
* University of Massachusetts Amherst
5+
* This file is part of the Coz project. See LICENSE.md file at the top-level
6+
* directory of this distribution and at http://github.com/plasma-umass/coz.
7+
*/
8+
9+
import PackageDescription
10+
11+
let package = Package(
12+
name: "coz",
13+
products: [
14+
.library(name: "Coz", targets: ["Coz"])
15+
],
16+
targets: [
17+
// C shim: resolves libcoz's exported symbols with dlsym and mirrors the
18+
// COZ_INCREMENT_COUNTER logic from include/coz.h.
19+
.target(
20+
name: "CCoz",
21+
cSettings: [
22+
// glibc only declares RTLD_DEFAULT under _GNU_SOURCE.
23+
.define("_GNU_SOURCE", .when(platforms: [.linux]))
24+
],
25+
linkerSettings: [
26+
.linkedLibrary("dl", .when(platforms: [.linux]))
27+
]
28+
),
29+
.target(name: "Coz", dependencies: ["CCoz"]),
30+
.executableTarget(name: "CozToy", dependencies: ["Coz"]),
31+
.testTarget(name: "CozTests", dependencies: ["Coz"]),
32+
]
33+
)

swift/README.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# coz-swift
2+
3+
Swift support for the [`coz` causal profiler](https://github.com/plasma-umass/coz).
4+
5+
A traditional profiler tells you *where* your program spends time. Coz tells you
6+
whether optimizing a line would actually make the program faster — which, in
7+
concurrent code, is a different question.
8+
9+
Works on **Linux** and **macOS**.
10+
11+
## Usage
12+
13+
First [install `coz`](https://github.com/plasma-umass/coz#installation), then add
14+
the package:
15+
16+
```swift
17+
.package(url: "https://github.com/plasma-umass/coz.git", from: "0.3.0")
18+
```
19+
20+
and depend on the `Coz` product:
21+
22+
```swift
23+
.target(name: "MyApp", dependencies: [.product(name: "Coz", package: "coz")])
24+
```
25+
26+
Mark the points where your program makes progress. For throughput — "I wish this
27+
happened more often":
28+
29+
```swift
30+
import Coz
31+
32+
for request in requests {
33+
handle(request)
34+
Coz.progress() // equivalent of COZ_PROGRESS
35+
}
36+
```
37+
38+
`Coz.progress("requests")` is the equivalent of `COZ_PROGRESS_NAMED`.
39+
40+
For latency — "I wish this finished sooner" — bracket the operation:
41+
42+
```swift
43+
try Coz.scope("request") {
44+
try handle(request)
45+
}
46+
```
47+
48+
`scope` fires its end counter even if the body throws. If you need the halves
49+
apart, `Coz.begin("request")` and `Coz.end("request")` are available.
50+
51+
On a hot path, hold a counter instead of looking it up by name each time:
52+
53+
```swift
54+
let requests = Coz.Counter(throughput: "requests")
55+
for request in batch {
56+
handle(request)
57+
requests.increment()
58+
}
59+
```
60+
61+
`Coz.isAvailable` reports whether the program is running under `coz run`.
62+
63+
## Building and running
64+
65+
Coz needs DWARF line tables, so build with debug information:
66+
67+
```
68+
swift build -c release -Xswiftc -g
69+
coz run --- .build/release/MyApp
70+
coz plot --text
71+
```
72+
73+
A plain `swift build` (debug) also works and carries debug info by default.
74+
75+
## Example
76+
77+
`Sources/CozToy` runs two threads per round; one does twice the work of the
78+
other, so it sits on the critical path.
79+
80+
```
81+
swift build -c release -Xswiftc -g
82+
coz run --- .build/release/CozToy
83+
coz plot --text
84+
```
85+
86+
```
87+
Source Line | Slope | R² | Max Speedup | Points
88+
-----------------------------------------+---------+-------+-------------+-------
89+
swift/Sources/CozToy/main.swift:44 | 0.616 | 0.90 | + 46.9% | 9
90+
swift/Sources/CozToy/main.swift:55 | 0.052 | 1.00 | + 4.7% | 2
91+
```
92+
93+
Line 44 is inside `slowWork`'s loop and line 55 inside `fastWork`'s. Coz predicts
94+
that speeding up `slowWork` speeds up the program roughly proportionally, and
95+
finds `fastWork` nearly irrelevant.
96+
97+
## Caveats
98+
99+
Both of these come down to the same thing: **coz only understands threads and
100+
blocking primitives that it interposes.** It interposes `pthread_create`,
101+
`pthread_join`, pthread mutexes and condition variables. Everything below follows
102+
from that.
103+
104+
**On macOS, coz cannot delay libdispatch's global queues.** Coz applies a virtual
105+
speedup by *delaying every other thread*, and it can only delay threads it knows
106+
about. On Darwin the global `DispatchQueue` workers are kernel-created
107+
pthread-workqueue threads that never call `pthread_create`. Coz will still
108+
*sample* them, so their lines appear in the profile, but the virtual speedup has
109+
nothing to slow down and every line comes out with a slope near zero.
110+
111+
If your hot work runs on `DispatchQueue.global()` and every line reads as flat,
112+
that is why. Run the code you want to profile on a `Thread` or a plain `pthread`,
113+
as `Sources/CozToy` does. This does not affect Linux, where
114+
swift-corelibs-libdispatch creates its workers with `pthread_create`.
115+
116+
**Blocking on a primitive coz cannot see will skew your results.** A thread
117+
blocked on a raw futex, a Mach semaphore, or a `DispatchSemaphore` still gets
118+
charged for the virtual delays it "should" have paid while it was asleep. If the
119+
thread that hits your progress point is the one doing the waiting, every line
120+
comes out with a *negative* slope. (This is not hypothetical — this example
121+
originally joined its threads with a `DispatchSemaphore` and reported exactly
122+
that.)
123+
124+
Either block on something coz interposes — `pthread_join`, a pthread mutex or
125+
condvar — or tell coz about the primitive yourself:
126+
127+
```swift
128+
Coz.preBlock()
129+
semaphore.wait()
130+
Coz.postBlock(skipDelays: true) // true: another thread woke us
131+
132+
Coz.catchUp() // pay delays before we may wake someone
133+
semaphore.signal()
134+
```
135+
136+
These map to `COZ_PRE_BLOCK`, `COZ_POST_BLOCK` and `COZ_CATCH_UP` in `coz.h`.
137+
138+
**Don't use a SIGPROF-based profiler at the same time.** Coz samples with
139+
`SIGPROF` and will fight anything else that wants it.
140+
141+
**Progress points must be reached often enough.** Coz needs roughly five
142+
progress-point visits per experiment, and each experiment runs for about half a
143+
second. A program that reaches its progress point a dozen times in total will
144+
produce very few data points.
145+
146+
**Swift optimizes counted loops aggressively.** A microbenchmark like
147+
`for i in 0..<n { acc &+= i }` gets folded to a closed form and deleted, leaving
148+
coz nothing to sample. This bites synthetic examples, not real programs.
149+
150+
## How it works
151+
152+
`Sources/CCoz/coz_shim.c` resolves `_coz_get_counter` and `_coz_add_delays` out
153+
of the injected `libcoz` with `dlsym(RTLD_DEFAULT, ...)`. That means the package
154+
needs neither `coz.h` at build time nor a system `coz-profiler` package, and a
155+
binary built against it runs normally when `libcoz` is absent — every entry point
156+
degrades to a no-op.
157+
158+
Counter lookups are memoized in C, so a hot progress point does not pay for
159+
libcoz's lock and name table on every hit, and the Swift side holds no global
160+
mutable state (the package builds clean under Swift 6 strict concurrency).
161+
162+
Incrementing is a relaxed atomic add. On macOS each progress point additionally
163+
calls `_coz_add_delays()`, because macOS has no per-thread sampling timer and a
164+
worker thread only discovers the virtual delay it owes when it reaches a progress
165+
point. This mirrors `_COZ_CHECK_DELAYS` in `include/coz.h`.

swift/Sources/CCoz/coz_shim.c

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright (c) 2015, Charlie Curtsinger and Emery Berger,
3+
* University of Massachusetts Amherst
4+
* This file is part of the Coz project. See LICENSE.md file at the top-level
5+
* directory of this distribution and at http://github.com/plasma-umass/coz.
6+
*/
7+
8+
/* RTLD_DEFAULT is a GNU extension on glibc. */
9+
#ifndef _GNU_SOURCE
10+
#define _GNU_SOURCE
11+
#endif
12+
13+
#include "coz_shim.h"
14+
15+
#include <dlfcn.h>
16+
#include <pthread.h>
17+
#include <stddef.h>
18+
#include <stdlib.h>
19+
#include <string.h>
20+
21+
typedef coz_counter_t *(*coz_get_counter_t)(int, const char *);
22+
typedef void (*coz_add_delays_t)(void);
23+
typedef void (*coz_pre_block_t)(void);
24+
typedef void (*coz_post_block_t)(int);
25+
26+
static coz_get_counter_t s_get_counter;
27+
static coz_add_delays_t s_add_delays;
28+
static coz_pre_block_t s_pre_block;
29+
static coz_post_block_t s_post_block;
30+
31+
static pthread_once_t s_once = PTHREAD_ONCE_INIT;
32+
33+
static void coz_shim_resolve(void) {
34+
/* libcoz is injected by `coz run` via LD_PRELOAD / DYLD_INSERT_LIBRARIES, so
35+
* its symbols live in the global scope and RTLD_DEFAULT finds them. Without
36+
* the profiler these stay NULL and every entry point degrades to a no-op. */
37+
s_get_counter = (coz_get_counter_t)dlsym(RTLD_DEFAULT, "_coz_get_counter");
38+
s_add_delays = (coz_add_delays_t)dlsym(RTLD_DEFAULT, "_coz_add_delays");
39+
s_pre_block = (coz_pre_block_t)dlsym(RTLD_DEFAULT, "_coz_pre_block");
40+
s_post_block = (coz_post_block_t)dlsym(RTLD_DEFAULT, "_coz_post_block");
41+
}
42+
43+
static void coz_shim_ensure(void) { pthread_once(&s_once, coz_shim_resolve); }
44+
45+
int coz_shim_available(void) {
46+
coz_shim_ensure();
47+
return s_get_counter != NULL;
48+
}
49+
50+
coz_counter_t *coz_shim_get_counter(int type, const char *name) {
51+
coz_shim_ensure();
52+
if (s_get_counter == NULL) return NULL;
53+
return s_get_counter(type, name);
54+
}
55+
56+
/* Counter cache.
57+
*
58+
* A program has a handful of progress points, so a linked list under a mutex is
59+
* ample. Entries are never removed, and the counters libcoz hands back are
60+
* stable for the life of the process, so a hit needs no further synchronization
61+
* beyond the atomic increment in coz_shim_hit(). */
62+
typedef struct coz_shim_entry {
63+
int type;
64+
char *name;
65+
coz_counter_t *counter;
66+
struct coz_shim_entry *next;
67+
} coz_shim_entry;
68+
69+
static coz_shim_entry *s_cache;
70+
static pthread_mutex_t s_cache_lock = PTHREAD_MUTEX_INITIALIZER;
71+
72+
coz_counter_t *coz_shim_cached_counter(int type, const char *name) {
73+
coz_counter_t *result = NULL;
74+
75+
pthread_mutex_lock(&s_cache_lock);
76+
for (coz_shim_entry *e = s_cache; e != NULL; e = e->next) {
77+
if (e->type == type && strcmp(e->name, name) == 0) {
78+
result = e->counter;
79+
pthread_mutex_unlock(&s_cache_lock);
80+
return result;
81+
}
82+
}
83+
84+
result = coz_shim_get_counter(type, name);
85+
86+
/* Cache misses (result == NULL, i.e. not profiling) too, so that an
87+
* un-profiled run does not call into dlsym-resolved code on every hit. */
88+
coz_shim_entry *entry = (coz_shim_entry *)malloc(sizeof(coz_shim_entry));
89+
if (entry != NULL) {
90+
entry->name = strdup(name);
91+
if (entry->name == NULL) {
92+
free(entry);
93+
} else {
94+
entry->type = type;
95+
entry->counter = result;
96+
entry->next = s_cache;
97+
s_cache = entry;
98+
}
99+
}
100+
pthread_mutex_unlock(&s_cache_lock);
101+
102+
return result;
103+
}
104+
105+
void coz_shim_hit(coz_counter_t *counter) {
106+
if (counter == NULL) return;
107+
108+
__atomic_add_fetch(&counter->count, 1, __ATOMIC_RELAXED);
109+
110+
#ifdef __APPLE__
111+
/* macOS has no per-thread sampling timer, so a worker thread only learns of
112+
* the virtual delay it owes when it reaches a progress point. On Linux the
113+
* SIGPROF handler already applies delays, and doing it again here would apply
114+
* them twice. This is the _COZ_CHECK_DELAYS split from include/coz.h. */
115+
if (s_add_delays != NULL) s_add_delays();
116+
#endif
117+
}
118+
119+
void coz_shim_pre_block(void) {
120+
coz_shim_ensure();
121+
if (s_pre_block != NULL) s_pre_block();
122+
}
123+
124+
void coz_shim_post_block(int skip_delays) {
125+
coz_shim_ensure();
126+
if (s_post_block != NULL) s_post_block(skip_delays);
127+
}
128+
129+
void coz_shim_catch_up(void) {
130+
coz_shim_ensure();
131+
if (s_add_delays != NULL) s_add_delays();
132+
}

0 commit comments

Comments
 (0)