Skip to content

Commit 413ac2d

Browse files
committed
feat: add @perf_event attach and counter helpers
- switch perf_event attachment to the perf_options builtin struct - add type checking and userspace codegen for attach, perf_read, and perf_print - update docs, examples, and tests for the new @perf_event workflow
1 parent a651add commit 413ac2d

12 files changed

Lines changed: 489 additions & 323 deletions

BUILTINS.md

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ fn main() -> i32 {
8383

8484
---
8585

86-
#### `attach(handle, target, flags)` / `attach(handle, attr)`
86+
#### `attach(handle, target, flags)` / `attach(handle, opts, flags)`
8787
**Signature:** `attach(handle: ProgramHandle, target: str(128), flags: u32) -> u32`
88-
**Signature:** `attach(handle: ProgramHandle, attr: perf_event_attr) -> u32`
88+
**Signature:** `attach(handle: ProgramHandle, opts: perf_options, flags: u32) -> u32`
8989
**Variadic:** No
9090
**Context:** Userspace only
9191

92-
**Description:** Attach a loaded eBPF program to a target interface or attachment point, or attach it to a perf event described by `perf_event_attr`.
92+
**Description:** Attach a loaded eBPF program to a target interface or attachment point, or to a perf event counter described by `perf_options`. Both forms take three arguments, keeping a uniform call shape across all program types.
9393

9494
**Parameters:**
9595
- Standard form:
@@ -98,7 +98,8 @@ fn main() -> i32 {
9898
- `flags`: Attachment flags (context-dependent)
9999
- Perf event form:
100100
- `handle`: Program handle returned from `load()`
101-
- `attr`: `perf_event_attr` value describing counter, pid, cpu, period, and filter flags
101+
- `opts`: `perf_options` value — only `counter` is required; all other fields have defaults
102+
- `flags`: Reserved (pass `0`)
102103

103104
**Return Value:**
104105
- Returns `0` on success
@@ -112,24 +113,17 @@ if (result != 0) {
112113
print("Failed to attach program")
113114
}
114115
115-
var perf_attr = perf_event_attr {
116-
counter: branch_misses,
117-
pid: -1,
118-
cpu: 0,
119-
period: 1000000,
120-
wakeup: 1,
121-
inherit: false,
122-
exclude_kernel: false,
123-
exclude_user: false
124-
}
125-
116+
// Minimal perf attach — all non-counter fields use defaults:
117+
// pid=-1 (all procs), cpu=0, period=1_000_000, wakeup=1, flags=false
126118
var perf_prog = load(on_branch_miss)
127-
attach(perf_prog, perf_attr)
119+
attach(perf_prog, perf_options { counter: branch_misses }, 0)
120+
var count = perf_read(perf_prog)
121+
detach(perf_prog)
128122
```
129123

130124
**Context-specific implementations:**
131125
- **eBPF:** Not available
132-
- **Userspace:** Uses `attach_bpf_program_by_fd` for standard targets and `ks_open_perf_event` for perf events
126+
- **Userspace:** Uses `attach_bpf_program_by_fd` for standard targets and `ks_attach_perf_event` for perf events
133127
- **Kernel Module:** Not available
134128

135129
---
@@ -359,7 +353,7 @@ fn main() -> i32 {
359353
|----------|------|-----------|---------------|-------|
360354
| `print()` |||| Different output destinations |
361355
| `load()` |||| Program management only |
362-
| `attach()` |||| Standard attach and perf_event_attr attach |
356+
| `attach()` |||| Standard attach and perf_options attach |
363357
| `detach()` |||| Program management only |
364358
| `register()` |||| struct_ops registration |
365359
| `test()` |||| Testing framework only |

README.md

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ fn main() -> i32 {
270270

271271
### Hardware Performance Counter Programs
272272

273-
Use `@perf_event` to attach eBPF programs to hardware or software performance counters. The userspace side describes the counter via a `perf_event_attr` struct literal and calls `attach(prog, attr)`:
273+
Use `@perf_event` to attach eBPF programs to hardware or software performance counters. Only `counter` is required in the `perf_options` struct; all other fields have sensible defaults. Call `attach(prog, perf_options { ... }, 0)` and read back the counter with `perf_read(prog)`:
274274

275275
```kernelscript
276276
// eBPF program fires on every hardware branch-miss sample
@@ -280,20 +280,16 @@ fn on_branch_miss(ctx: *bpf_perf_event_data) -> i32 {
280280
}
281281
282282
fn main() -> i32 {
283-
var attr = perf_event_attr {
284-
counter: branch_misses, // hardware counter (see perf_counter enum)
285-
pid: -1, // all processes
286-
cpu: 0, // CPU 0
287-
period: 1000000, // sample every 1 million events
288-
wakeup: 1,
289-
inherit: false,
290-
exclude_kernel: false,
291-
exclude_user: false
292-
}
293-
294283
var prog = load(on_branch_miss)
295-
attach(prog, attr) // opens perf_event_open fd, resets, attaches BPF, enables
296-
detach(prog) // disables counter, destroys BPF link, closes fd
284+
285+
// Minimal form — defaults: pid=-1 (all procs), cpu=0,
286+
// period=1_000_000, wakeup=1, all flags=false
287+
attach(prog, perf_options { counter: branch_misses }, 0)
288+
289+
var count = perf_read(prog) // read counter via program handle
290+
print(count)
291+
292+
detach(prog) // disables counter, destroys BPF link, closes fd
297293
return 0
298294
}
299295
```

SPEC.md

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ kernelscript init tracepoint/syscalls/sys_enter_read my_syscall_tracer
442442

443443
#### 3.1.3 Perf Event Programs
444444

445-
`@perf_event` programs attach eBPF logic to hardware or software performance counters via `perf_event_open(2)`. The eBPF function is invoked for every counter sample; the userspace side controls which counter to monitor through a `perf_event_attr` struct literal passed to `attach()`.
445+
`@perf_event` programs attach eBPF logic to hardware or software performance counters via `perf_event_open(2)`. The eBPF function is invoked for every counter sample; the userspace side controls which counter to monitor through a `perf_options` struct literal passed to the standard 3-argument `attach()`.
446446

447447
**Syntax:**
448448
```kernelscript
@@ -458,25 +458,41 @@ The context type is always `*bpf_perf_event_data` (from `vmlinux.h`).
458458
**Userspace lifecycle:**
459459
```kernelscript
460460
fn main() -> i32 {
461-
var attr = perf_event_attr {
462-
counter: branch_misses, // perf_counter enum value
463-
pid: -1, // -1 = all processes; ≥0 = specific PID
464-
cpu: 0, // ≥0 = specific CPU; -1 = any CPU (pid must be ≥0)
465-
period: 1000000, // sample after this many events (0 → default 1000000)
466-
wakeup: 1, // wake userspace after N samples (0 → default 1)
467-
inherit: false, // inherit to forked children
468-
exclude_kernel: false, // exclude kernel-mode samples
469-
exclude_user: false // exclude user-mode samples
470-
}
471-
472461
var prog = load(my_handler)
473-
attach(prog, attr) // perf_event_open → IOC_RESET → attach BPF → IOC_ENABLE
474-
// ... run workload ...
475-
detach(prog) // IOC_DISABLE → bpf_link__destroy → close(perf_fd)
462+
463+
// Only counter is required; all other fields use language-level defaults:
464+
// pid=-1, cpu=0, period=1_000_000, wakeup=1, inherit/exclude_*=false
465+
attach(prog, perf_options { counter: branch_misses }, 0)
466+
467+
// Override specific fields as needed:
468+
attach(prog, perf_options {
469+
counter: cache_misses,
470+
cpu: 2,
471+
period: 500000,
472+
exclude_kernel: true,
473+
}, 0)
474+
475+
var count = perf_read(prog) // read counter value via program handle
476+
print(count)
477+
478+
detach(prog) // IOC_DISABLE → bpf_link__destroy → close(perf_fd)
476479
return 0
477480
}
478481
```
479482

483+
**`perf_options` fields and defaults:**
484+
485+
| Field | Type | Default | Description |
486+
|---|---|---|---|
487+
| `counter` | `perf_counter` | *(required)* | Hardware/software counter |
488+
| `pid` | `i32` | `-1` | -1 = all processes; ≥0 = specific PID |
489+
| `cpu` | `i32` | `0` | ≥0 = specific CPU; -1 = any CPU (pid must be ≥0) |
490+
| `period` | `u64` | `1000000` | Sample after this many events |
491+
| `wakeup` | `u32` | `1` | Wake userspace after N samples |
492+
| `inherit` | `bool` | `false` | Inherit to forked children |
493+
| `exclude_kernel` | `bool` | `false` | Exclude kernel-mode samples |
494+
| `exclude_user` | `bool` | `false` | Exclude user-mode samples |
495+
480496
**`pid` / `cpu` rules enforced at runtime:**
481497

482498
| `pid` | `cpu` | Meaning |
@@ -500,15 +516,17 @@ fn main() -> i32 {
500516
| `context_switches` | `PERF_COUNT_SW_CONTEXT_SWITCHES` |
501517
| `cpu_migrations` | `PERF_COUNT_SW_CPU_MIGRATIONS` |
502518

503-
**Generated C helpers (emitted when `attach(prog, attr)` is used):**
519+
**Generated C helpers (emitted when `attach(prog, perf_options{...}, flags)` is used):**
504520

505521
| Function | Signature | Description |
506522
|---|---|---|
507-
| `ks_open_perf_event` | `int (ks_perf_event_attr)` | Calls `perf_event_open(2)`, returns fd |
523+
| `ks_open_perf_event` | `int (ks_perf_options)` | Calls `perf_event_open(2)`, returns fd |
524+
| `ks_attach_perf_event` | `int (int prog_fd, ks_perf_options, int flags)` | Full open-reset-attach-enable lifecycle |
508525
| `ks_read_perf_count` | `int64_t (int perf_fd)` | Reads current 64-bit counter via `read()` |
509-
| `ks_print_perf_count` | `void (int perf_fd, const char*)` | Prints `[perf] <name>: <count>` to stdout |
526+
| `ks_perf_read` | `int64_t (int prog_fd)` | High-level read via program handle |
527+
| `ks_perf_print` | `void (int prog_fd, const char*)` | Prints `[perf] <name>: <count>` to stdout |
510528

511-
**Attach sequence (compiler-generated):**
529+
**Attach sequence (compiler-generated, inside `ks_attach_perf_event`):**
512530
1. `ks_attr.attr.disabled = 1` — open counter without starting it
513531
2. `syscall(SYS_perf_event_open, ...)``perf_fd`
514532
3. `ioctl(perf_fd, PERF_EVENT_IOC_RESET, 0)` — zero the counter
@@ -521,7 +539,8 @@ fn main() -> i32 {
521539
3. `close(perf_fd)` — release the kernel perf event
522540

523541
**Compiler implementation:**
524-
- Detects `attach(prog, perf_event_attr_value)` call (two-argument form) and emits `ks_open_perf_event` + `attach_bpf_program_by_fd` sequence
542+
- Detects `attach(prog, perf_options_value, flags)` (three-argument form with `perf_options` second arg) and routes to `ks_attach_perf_event`
543+
- Exposes omitted `perf_options` fields as language-level defaults (partial struct literal)
525544
- Validates `pid ≥ -1`, `cpu ≥ -1`, and rejects `pid == -1 && cpu == -1` at runtime
526545
- Emits `PERF_FLAG_FD_CLOEXEC` for safe fd inheritance
527546
- BPF program section is `SEC("perf_event")`

examples/perf_branch_miss.ks

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,15 @@ fn on_branch_miss(ctx: *bpf_perf_event_data) -> i32 {
99
}
1010

1111
fn main() -> i32 {
12-
var attr = perf_event_attr {
13-
counter: branch_misses,
14-
pid: -1,
15-
cpu: 0,
16-
period: 1000000,
17-
wakeup: 1,
18-
inherit: false,
19-
exclude_kernel: false,
20-
exclude_user: false
21-
}
22-
2312
var prog = load(on_branch_miss)
24-
attach(prog, attr)
25-
detach(prog)
2613

14+
// Only counter is required; pid, cpu, period, wakeup and flag fields
15+
// default to: pid=-1 (all procs), cpu=0, period=1_000_000, wakeup=1,
16+
// inherit/exclude_kernel/exclude_user=false.
17+
attach(prog, perf_options { counter: branch_misses }, 0)
18+
19+
perf_print(prog, "branch_misses")
20+
21+
detach(prog)
2722
return 0
2823
}

examples/perf_cache_miss.ks

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// perf_cache_miss.ks
2+
// Demonstrates @perf_event program type in KernelScript.
3+
// The eBPF program runs on every hardware cache-miss event.
4+
// The userspace side opens the perf event and attaches the BPF program.
5+
6+
@perf_event
7+
fn on_cache_miss(ctx: *bpf_perf_event_data) -> i32 {
8+
return 0
9+
}
10+
11+
fn main() -> i32 {
12+
var prog = load(on_cache_miss)
13+
14+
// Only counter is required; pid, cpu, period, wakeup and flag fields
15+
// default to: pid=-1 (all procs), cpu=0, period=1_000_000, wakeup=1,
16+
// inherit/exclude_kernel/exclude_user=false.
17+
attach(prog, perf_options { counter: cache_misses,period: 10000000, inherit: true }, 0)
18+
19+
perf_print(prog, "cache_misses")
20+
21+
detach(prog)
22+
return 0
23+
}

src/btf_parser.ml

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ let generate_kernelscript_source ?extra_param ?include_kfuncs template project_n
506506
| None -> ""
507507
in
508508

509-
(* perf_event programs use a completely different main() with attach(prog, attr) *)
509+
(* perf_event programs use a completely different main() with attach(prog, opts, 0) *)
510510
if template.program_type = "perf_event" then
511511
sprintf {|%s
512512
// Generated by KernelScript compiler with direct BTF parsing%s
@@ -519,19 +519,14 @@ let generate_kernelscript_source ?extra_param ?include_kfuncs template project_n
519519
}
520520

521521
fn main() -> i32 {
522-
var attr = perf_event_attr {
523-
counter: branch_misses,
524-
pid: -1,
525-
cpu: 0,
526-
period: 1000000,
527-
wakeup: 1,
528-
inherit: false,
529-
exclude_kernel: false,
530-
exclude_user: false
531-
}
532-
533522
var prog = load(%s)
534-
attach(prog, attr)
523+
524+
// Only counter is required; all other fields default to sensible values.
525+
attach(prog, perf_options { counter: branch_misses }, 0)
526+
527+
var count = perf_read(prog)
528+
print(count)
529+
535530
detach(prog)
536531

537532
return 0

src/codegen_common.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ let rec ir_type_to_c target = function
4343
| UserspaceStd -> "char") (* Base type for userspace string - size handled in declaration *)
4444
| IRPointer (inner_type, _) -> sprintf "%s*" (ir_type_to_c target inner_type)
4545
| IRArray (inner_type, size, _) -> sprintf "%s[%d]" (ir_type_to_c target inner_type) size
46-
| IRStruct ("perf_event_attr", _) -> "ks_perf_event_attr" (* Avoid conflict with linux/perf_event.h *)
46+
| IRStruct ("perf_options", _) -> "ks_perf_options" (* Namespace KS type away from kernel structs *)
4747
| IRStruct (name, _) -> sprintf "struct %s" name
4848
| IREnum (name, _) -> sprintf "enum %s" name
4949
| IRResult (ok_type, _err_type) -> ir_type_to_c target ok_type (* simplified to ok type *)

src/stdlib.ml

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,17 @@ let validate_register_function arg_types ast_context _pos =
109109
| _ ->
110110
(false, Some "register() requires an impl block argument")
111111

112-
(** Validation function for attach() - accepts either standard 3-arg form or perf 2-arg form *)
112+
(** Validation function for attach() - accepts standard 3-arg form, and perf_options 3-arg form *)
113113
let validate_attach_function arg_types _ast_context _pos =
114114
match arg_types with
115115
| [ProgramHandle; Str _; (U8|U16|U32|U64|I8|I16|I32|I64)] ->
116116
(* Standard form: attach(prog, target, flags) *)
117117
(true, None)
118-
| [ProgramHandle; Struct "perf_event_attr"] | [ProgramHandle; UserType "perf_event_attr"] ->
119-
(* Perf event form: attach(prog, perf_event_attr) - compiler detects and routes appropriately *)
118+
| [ProgramHandle; (Struct "perf_options" | UserType "perf_options"); (U8|U16|U32|U64|I8|I16|I32|I64)] ->
119+
(* Perf event form: attach(prog, perf_options { ... }, flags) - uniform 3-arg shape *)
120120
(true, None)
121121
| _ ->
122-
(false, Some "attach() requires either (handle, target, flags) or (handle, perf_event_attr)")
122+
(false, Some "attach() requires (handle, target, flags) — target is a string or perf_options { ... }")
123123

124124
(** Standard library built-in functions *)
125125
let builtin_functions = [
@@ -147,9 +147,9 @@ let builtin_functions = [
147147
};
148148
{
149149
name = "attach";
150-
param_types = []; (* Custom validation handles both standard and perf_event forms *)
150+
param_types = []; (* Custom validation handles both standard and perf_options forms *)
151151
return_type = U32; (* Returns 0 on success *)
152-
description = "Attach a loaded eBPF program to a target with flags, or to a perf event counter";
152+
description = "Attach a loaded eBPF program to a target with flags; target is a string or perf_options { ... }";
153153
is_variadic = false;
154154
ebpf_impl = ""; (* Not available in eBPF context *)
155155
userspace_impl = "bpf_prog_attach";
@@ -222,6 +222,28 @@ let builtin_functions = [
222222
kernel_impl = ""; (* Not available in kernel context *)
223223
validate = Some validate_exec_function;
224224
};
225+
{
226+
name = "perf_read";
227+
param_types = [ProgramHandle];
228+
return_type = I64; (* Raw counter value, or -1 on error *)
229+
description = "Read the current hardware/software counter value for a perf_event program";
230+
is_variadic = false;
231+
ebpf_impl = ""; (* Not available in eBPF context *)
232+
userspace_impl = "ks_perf_read";
233+
kernel_impl = "";
234+
validate = None;
235+
};
236+
{
237+
name = "perf_print";
238+
param_types = [ProgramHandle; Str 128];
239+
return_type = Void;
240+
description = "Print the current counter value for a perf_event program with a label";
241+
is_variadic = false;
242+
ebpf_impl = ""; (* Not available in eBPF context *)
243+
userspace_impl = "ks_perf_print";
244+
kernel_impl = "";
245+
validate = None;
246+
};
225247

226248
]
227249

@@ -300,8 +322,9 @@ let builtin_types = [
300322
("cpu_migrations", Some (Ast.Signed64 8L));
301323
], builtin_pos));
302324

303-
(* perf_event_attr: KernelScript struct for specifying perf event configuration *)
304-
TypeDef (StructDef ("perf_event_attr", [
325+
(* perf_options: configuration bag for @perf_event programs.
326+
Only 'counter' is required; all other fields have language-level defaults. *)
327+
TypeDef (StructDef ("perf_options", [
305328
("counter", Enum "perf_counter");
306329
("pid", I32);
307330
("cpu", I32);
@@ -313,6 +336,23 @@ let builtin_types = [
313336
], builtin_pos));
314337
]
315338

339+
(** Default field values for structs that support partial initialisation.
340+
Returns [(field_name, default_literal)] for optional fields only.
341+
Required fields (e.g. counter in perf_options) are absent from the list,
342+
so the type checker will still error if they are omitted. *)
343+
let get_struct_field_defaults = function
344+
| "perf_options" ->
345+
Some [
346+
("pid", IntLit (Signed64 (-1L), None));
347+
("cpu", IntLit (Signed64 0L, None));
348+
("period", IntLit (Unsigned64 1000000L, None));
349+
("wakeup", IntLit (Unsigned64 1L, None));
350+
("inherit", BoolLit false);
351+
("exclude_kernel", BoolLit false);
352+
("exclude_user", BoolLit false);
353+
]
354+
| _ -> None
355+
316356
(** Get all builtin type definitions *)
317357
let get_builtin_types () = builtin_types
318358

0 commit comments

Comments
 (0)