diff --git a/BUILTINS.md b/BUILTINS.md index 9a18af3..09a7915 100644 --- a/BUILTINS.md +++ b/BUILTINS.md @@ -98,7 +98,7 @@ fn main() -> i32 { - `flags`: Attachment flags (context-dependent) - Perf event form: - `handle`: Program handle returned from `load()` - - `opts`: `perf_options` value — only `perf_type` and `perf_config` are required; all other fields have defaults + - `opts`: `perf_options` value — only `perf_type` and `perf_config` are required; all other fields have defaults, including no group (`group` invalid and `group_fd=-1`) - `flags`: Must be `0` for perf attaches; nonzero values are rejected **Return Value:** @@ -120,8 +120,20 @@ var perf_att = attach(perf_prog, perf_options { perf_type: perf_type_hardware, p var count = read(perf_att) detach(perf_att) detach(perf_prog) + +// Grouped perf events: branch joins cache's leader group. Adding a member restarts the group. +var cache = attach(perf_prog, perf_options { perf_type: perf_type_hardware, perf_config: cache_misses }, 0) +var branch = attach(perf_prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group: cache, +}, 0) +detach(branch) +detach(cache) ``` +Grouped events are scheduled as one atomic PMU unit. Separate events and separate groups may be multiplexed, but members inside one group cannot be independently multiplexed. Static groups that exceed the target PMU counter limit are rejected at compile time; override the detected/default limit with `KERNELSCRIPT_PERF_GROUP_MAX_EVENTS` when compiling for a different target. + **Context-specific implementations:** - **eBPF:** Not available - **Userspace:** Uses `attach_bpf_program_by_fd` for standard targets and `ks_attach_perf_event` for perf events @@ -163,15 +175,60 @@ detach(prog) // Clean up **Variadic:** No **Context:** Userspace only -**Description:** Read the current hardware/software counter value from a perf attachment. +**Description:** Read the current hardware/software counter value from a perf attachment. If the kernel multiplexed the event, the value is scaled with `time_enabled / time_running`. **Parameters:** - `handle`: Perf attachment returned from `attach(handle, perf_options, flags)` **Return Value:** -- Returns the raw 64-bit counter value on success +- Returns the raw 64-bit counter value when no multiplexing occurred +- Returns a scaled value when `time_running < time_enabled` - Returns `-1` on invalid/stale attachment or read failure - Reads use the attachment's `perf_fd` directly; the internal token detects copied handles used after detach. +- Use `read_group(leader)` when you need a same-time group snapshot. + +--- + +#### `read_raw(handle)` +**Signature:** `read_raw(handle: PerfAttachment) -> i64` +**Variadic:** No +**Context:** Userspace only + +**Description:** Read the unscaled raw hardware/software counter value from a perf attachment. + +**Return Value:** +- Returns the raw counter value +- Returns `-1` on invalid/stale attachment or read failure + +--- + +#### `read_details(handle)` +**Signature:** `read_details(handle: PerfAttachment) -> PerfReadDetails` +**Variadic:** No +**Context:** Userspace only + +**Description:** Read raw, scaled, `time_enabled`, and `time_running` details for a perf attachment. + +**Return Value:** +- `raw`: unscaled counter value +- `scaled`: multiplex-corrected value, or `-1` on timing/read error +- `time_enabled`: perf enabled time +- `time_running`: perf running time + +--- + +#### `read_group(leader)` +**Signature:** `read_group(leader: PerfAttachment) -> PerfGroupRead` +**Variadic:** No +**Context:** Userspace only + +**Description:** Read a same-time snapshot from a perf event group leader. This enables `PERF_FORMAT_GROUP | PERF_FORMAT_ID` in generated perf events. + +**Return Value:** +- `count`: number of entries returned, capped at 16 +- `values`: multiplex-scaled values from the snapshot +- `ids`: perf event IDs for the returned values +- `time_enabled` / `time_running`: timing fields used for scaling --- diff --git a/README.md b/README.md index 4780749..20f8abc 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ fn on_branch_miss(ctx: *bpf_perf_event_data) -> i32 { fn main() -> i32 { var prog = load(on_branch_miss) - // Minimal form — defaults: pid=-1 (all procs), cpu=0, + // Minimal form — defaults: pid=-1 (all procs), cpu=0, no group, // period=1_000_000, wakeup=1; perf attach flags must be 0 var att = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0) var count = read(att) @@ -318,6 +318,22 @@ fn main() -> i32 { } ``` +Perf events can share a kernel scheduling group by passing the leader attachment directly with `group`. +The lower-level `group_fd: cache.perf_fd` form is still supported for compatibility: + +```kernelscript +var cache = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: cache_misses }, 0) +var branch = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group: cache, +}, 0) +``` + +Adding a member restarts the whole group from zero. Detaching a leader cascades to any live members. A group competes for PMU counters as one atomic unit: different groups can be multiplexed over time, but members inside one group are not independently multiplexed. For statically visible groups, the compiler rejects groups that need more PMU counter slots than the target limit. The limit is read from known sysfs PMU caps when available, defaults to 4, and can be overridden with `KERNELSCRIPT_PERF_GROUP_MAX_EVENTS`. + +`read(att)` returns a multiplex-scaled count when the kernel reports `time_running < time_enabled`. Use `read_raw(att)` for the raw value, `read_details(att)` for raw/scaled/timing details, and `read_group(leader)` for a same-time group snapshot. + **Available `perf_type` values:** | Enum value | Hardware/software event | diff --git a/SPEC.md b/SPEC.md index 92d39ab..a062b34 100644 --- a/SPEC.md +++ b/SPEC.md @@ -461,7 +461,7 @@ fn main() -> i32 { var prog = load(my_handler) // Only perf_type + perf_config are required; all other fields use language-level defaults: - // pid=-1, cpu=0, period=1_000_000, wakeup=1, inherit/exclude_*=false + // pid=-1, cpu=0, no group, period=1_000_000, wakeup=1, inherit/exclude_*=false var misses = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0) // Override specific fields as needed: @@ -473,8 +473,19 @@ fn main() -> i32 { exclude_kernel: true, }, 0) - print("misses=%lld cache=%lld", read(misses), read(cache)) + // Put branch misses in cache's perf event group. Adding a member restarts + // the whole group from zero. The lower-level group_fd: cache.perf_fd form + // is still accepted. + var branch = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group: cache, + }, 0) + print("misses=%lld cache=%lld branch=%lld", read(misses), read(cache), read(branch)) + var snapshot = read_group(cache) + + detach(branch) detach(cache) // IOC_DISABLE → bpf_link__destroy → close(perf_fd) detach(misses) detach(prog) @@ -490,6 +501,8 @@ fn main() -> i32 { | `perf_config` | `u64` | *(required)* | `perf_event_attr.config` value for that type | | `pid` | `i32` | `-1` | -1 = all processes; ≥0 = specific PID | | `cpu` | `i32` | `0` | ≥0 = specific CPU; -1 = any CPU (pid must be ≥0) | +| `group_fd` | `i32` | `-1` | -1 = standalone event; ≥0 = perf group leader fd | +| `group` | `PerfAttachment` | invalid attachment | Preferred high-level group leader attachment | | `period` | `u64` | `1000000` | Sample after this many events | | `wakeup` | `u32` | `1` | Wake userspace after N samples | | `inherit` | `bool` | `false` | Inherit to forked children | @@ -538,16 +551,39 @@ For event families with a richer config space, such as `perf_type_hw_cache`, pro |---|---|---| | `ks_open_perf_event` | `int (ks_perf_options)` | Calls `perf_event_open(2)`, returns fd | | `ks_attach_perf_event` | `PerfAttachment (int prog_fd, ks_perf_options, int flags)` | Full open-reset-attach-enable lifecycle | -| `ks_read_perf_count` | `int64_t (int perf_fd)` | Reads current 64-bit counter via `read()` | +| `ks_read_perf_count` | `int64_t (int perf_fd)` | Reads current counter and applies multiplex scaling when needed | | `ks_perf_attachment_read` | `int64_t (PerfAttachment)` | Direct fd read through the attachment value with stale-handle detection | +| `ks_perf_attachment_read_raw` | `int64_t (PerfAttachment)` | Direct raw counter read with stale-handle detection | +| `ks_perf_attachment_read_details` | `PerfReadDetails (PerfAttachment)` | Returns raw, scaled, `time_enabled`, and `time_running` | +| `ks_perf_attachment_read_group` | `PerfGroupRead (PerfAttachment)` | Reads a same-time group snapshot from a leader attachment | -**Attach sequence (compiler-generated, inside `ks_attach_perf_event`):** +**Attach sequence for standalone events (compiler-generated, inside `ks_attach_perf_event`):** 1. `ks_attr.attr.disabled = 1` — open counter without starting it -2. `syscall(SYS_perf_event_open, ...)` → `perf_fd` +2. `syscall(SYS_perf_event_open, ..., group_fd=-1, ...)` → `perf_fd` 3. `ioctl(perf_fd, PERF_EVENT_IOC_RESET, 0)` — zero the counter 4. `bpf_program__attach_perf_event(prog, perf_fd)` — link BPF program 5. `ioctl(perf_fd, PERF_EVENT_IOC_ENABLE, 0)` — **start counting** +**Perf event groups:** +- `group: leader_attachment` is the preferred way to join a perf group. +- `group_fd >= 0` opens the new event as a member of that leader fd. +- Group members are opened disabled, linked to the BPF program, then the leader is disabled, reset, and enabled with `PERF_IOC_FLAG_GROUP`. +- Adding a member to an already running group restarts the whole group from zero. +- A group is scheduled as an atomic PMU unit. Separate events and separate groups may be multiplexed; members inside one group are not independently multiplexed. If a statically visible group needs more PMU counter slots than the target limit, compilation fails. +- The compile-time group limit uses known sysfs PMU caps when available, falls back to `4`, and can be overridden with `KERNELSCRIPT_PERF_GROUP_MAX_EVENTS`. +- `perf_type_software` and `perf_type_tracepoint` do not consume PMU counter slots for this check; static hardware/raw/cache/breakpoint events consume one slot, and dynamic `perf_type` values are conservatively counted as one slot. +- Detaching a member is allowed. Detaching a leader cascades to any live members. +- `read_group(leader)` enables `PERF_FORMAT_GROUP | PERF_FORMAT_ID` and returns up to 16 same-time group values plus perf IDs and timing fields. + +**Counter reads:** +- Generated perf events request `PERF_FORMAT_TOTAL_TIME_ENABLED | PERF_FORMAT_TOTAL_TIME_RUNNING`. +- `read(att)` returns the raw value when `time_enabled == time_running`. +- If multiplexing occurred, `read(att)` returns `value * time_enabled / time_running` using a 128-bit intermediate. +- If `time_running == 0`, `read(att)` reports an error and returns `-1`. +- `read_raw(att)` returns the unscaled raw counter. +- `read_details(att)` returns raw, scaled, `time_enabled`, and `time_running`. +- `read_group(leader)` returns a snapshot struct; group `values[]` are scaled using the snapshot timing fields. + **Detach sequence (compiler-generated):** 1. `ioctl(perf_fd, PERF_EVENT_IOC_DISABLE, 0)` — stop counting 2. `bpf_link__destroy(link)` — unlink BPF program @@ -559,7 +595,8 @@ For event families with a richer config space, such as `perf_type_hw_cache`, pro - Returns a first-class `PerfAttachment` value for perf attaches so one program can hold multiple live counters - `PerfAttachment` carries `perf_fd` plus an internal generation token; `read(attachment)` avoids global attachment-list scans and rejects copied handles after detach - Exposes omitted `perf_options` fields as language-level defaults (partial struct literal) -- Validates `pid ≥ -1`, `cpu ≥ -1`, and rejects `pid == -1 && cpu == -1` at runtime +- Validates `pid ≥ -1`, `cpu ≥ -1`, `group_fd ≥ -1`, and rejects `pid == -1 && cpu == -1` at runtime +- Treats `group` as valid only when it carries a live `PerfAttachment` generation token; otherwise `group_fd` controls grouping - Emits `PERF_FLAG_FD_CLOEXEC` for safe fd inheritance - BPF program section is `SEC("perf_event")` diff --git a/examples/perf_cache_miss.ks b/examples/perf_cache_miss.ks index 89bd50e..6afe209 100644 --- a/examples/perf_cache_miss.ks +++ b/examples/perf_cache_miss.ks @@ -11,19 +11,48 @@ fn on_cache_miss(ctx: *bpf_perf_event_data) -> i32 { fn main() -> i32 { var prog = load(on_cache_miss) - // Only perf_type + perf_config are required; pid, cpu, period, wakeup and flag fields + // Only perf_type + perf_config are required; pid, cpu, group/group_fd, period, wakeup and flag fields // default to: pid=-1 (all procs), cpu=0, period=1_000_000, wakeup=1, - // inherit/exclude_kernel/exclude_user=false. + // no group, inherit/exclude_kernel/exclude_user=false. var cache = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: cache_misses, period: 10000000, inherit: true }, 0) - var branch = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses, period: 10000000, inherit: true }, 0) + // branch joins cache's perf event group. Adding a member restarts the whole group from zero. + var branch = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses, period: 10000000, inherit: true, group: cache }, 0) print("Cache-miss and branch-miss perf_event demo attached") var cache_count = read(cache) print("Cache-miss count: %lld", cache_count) var branch_count = read(branch) print("Branch-miss count: %lld", branch_count) + + var prev = read_details(cache) + // Simulate workload with cache misses and branch misses. + var x = 0 + var i = 0 + for (i in 0..10000000) { + if (i % 100 == 0) { + x = x + 1 + } else { + x = x * 2 + } + } + var cur = read_details(cache) + var delta = cur.scaled - prev.scaled + var dt_ns = cur.time_enabled - prev.time_enabled + if (dt_ns > 0) { + var per_sec = (delta * 1000000000) / dt_ns + print("Cache misses/sec: %lld", per_sec) + } + + var snapshot = read_group(cache) + print("Grouped snapshot entries: %u", snapshot.count) + + var snapshot_index = 0 + while (snapshot_index < snapshot.count) { + print("id=%llu value=%lld", snapshot.ids[snapshot_index], snapshot.values[snapshot_index]) + snapshot_index = snapshot_index + 1 + } - detach(cache) detach(branch) + detach(cache) detach(prog) print("Cache-miss and branch-miss perf_event demo detached") return 0 diff --git a/examples/perf_page_fault.ks b/examples/perf_page_fault.ks index 79fd266..7857b15 100644 --- a/examples/perf_page_fault.ks +++ b/examples/perf_page_fault.ks @@ -14,8 +14,11 @@ fn main() -> i32 { // pid: 0 = current process, cpu: -1 = any CPU (standard per-process monitoring). // page_faults (PERF_COUNT_SW_PAGE_FAULTS) is the most reliable software event: // every heap/stack allocation triggers minor page faults, no scheduler dependency. - var att = attach(prog, perf_options { perf_type: perf_type_software, perf_config: page_faults, pid: 0, cpu: -1, period: 1 }, 0) - print("Page-fault perf_event demo attached") + var page = attach(prog, perf_options { perf_type: perf_type_software, perf_config: page_faults, pid: 0, cpu: -1, period: 1 }, 0) + // branch joins cache's perf event group. Adding a member restarts the whole group from zero. + var branch = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses, period: 10000000, inherit: true}, 0) + + print("perf_event demo attached") // Repeatedly increment a counter; stack/heap activity will generate page faults. var x: i64 = 0 @@ -23,11 +26,14 @@ fn main() -> i32 { x = x + 1 } - var count = read(att) - print("Page-fault count: %lld", count) + var page_fault_count = read(page) + print("Page-fault count: %lld", page_fault_count) + var branch_count = read(branch) + print("Branch-miss count: %lld", branch_count) - detach(att) - print("Page-fault perf_event demo detached") + detach(page) + detach(branch) + print("perf_event demo detached") detach(prog) return 0 } diff --git a/src/ir_generator.ml b/src/ir_generator.ml index 6b5bcf7..e828e51 100644 --- a/src/ir_generator.ml +++ b/src/ir_generator.ml @@ -877,7 +877,7 @@ let rec lower_expression ctx (expr : Ast.expr) = emit_variable_decl_val ctx ptr_val ptr_val.val_type (Some ptr_expr) expr.expr_pos; (* result = *ptr *) - let load_expr = make_ir_expr (IRValue ptr_val) element_type expr.expr_pos in + let load_expr = make_ir_expr (IRUnOp (IRDeref, ptr_val)) element_type expr.expr_pos in emit_variable_decl_val ctx result_val element_type (Some load_expr) expr.expr_pos); result_val) @@ -3572,4 +3572,4 @@ let generate_ir ?(use_type_annotations=false) ast symbol_table source_name = with | exn -> Printf.eprintf "IR generation failed: %s\n" (Printexc.to_string exn); - raise exn \ No newline at end of file + raise exn diff --git a/src/stdlib.ml b/src/stdlib.ml index f2d2855..d291f14 100644 --- a/src/stdlib.ml +++ b/src/stdlib.ml @@ -129,6 +129,13 @@ let validate_read_function arg_types _ast_context _pos = | _ -> (false, Some "read() currently requires a PerfAttachment") +let validate_read_group_function arg_types _ast_context _pos = + match arg_types with + | [Struct "PerfAttachment"] | [UserType "PerfAttachment"] -> + (true, None) + | _ -> + (false, Some "read_group() requires a PerfAttachment group leader") + (** Validation function for detach() - accepts program handles and perf attachments *) let validate_detach_function arg_types _ast_context _pos = match arg_types with @@ -244,13 +251,46 @@ let builtin_functions = [ name = "read"; param_types = []; (* Custom validation handles attachment-aware overloads *) return_type = I64; (* Raw counter value, or -1 on error *) - description = "Read the current hardware/software counter value for a perf attachment"; + description = "Read the multiplex-scaled hardware/software counter value for a perf attachment"; is_variadic = false; ebpf_impl = ""; (* Not available in eBPF context *) userspace_impl = "ks_perf_attachment_read"; kernel_impl = ""; validate = Some validate_read_function; }; + { + name = "read_raw"; + param_types = []; + return_type = I64; + description = "Read the raw hardware/software counter value for a perf attachment"; + is_variadic = false; + ebpf_impl = ""; + userspace_impl = "ks_perf_attachment_read_raw"; + kernel_impl = ""; + validate = Some validate_read_function; + }; + { + name = "read_details"; + param_types = []; + return_type = Struct "PerfReadDetails"; + description = "Read raw, scaled, time_enabled, and time_running for a perf attachment"; + is_variadic = false; + ebpf_impl = ""; + userspace_impl = "ks_perf_attachment_read_details"; + kernel_impl = ""; + validate = Some validate_read_function; + }; + { + name = "read_group"; + param_types = []; + return_type = Struct "PerfGroupRead"; + description = "Read a same-time snapshot from a perf event group leader"; + is_variadic = false; + ebpf_impl = ""; + userspace_impl = "ks_perf_attachment_read_group"; + kernel_impl = ""; + validate = Some validate_read_group_function; + }; ] (** Get built-in function definition by name *) @@ -349,6 +389,8 @@ let builtin_types = [ ("perf_config", U64); ("pid", I32); ("cpu", I32); + ("group_fd", I32); + ("group", Struct "PerfAttachment"); ("period", U64); ("wakeup", U32); ("inherit", Bool); @@ -363,6 +405,21 @@ let builtin_types = [ ("prog_fd", I32); ("generation", U64); ], builtin_pos)); + + TypeDef (StructDef ("PerfReadDetails", [ + ("raw", I64); + ("scaled", I64); + ("time_enabled", U64); + ("time_running", U64); + ], builtin_pos)); + + TypeDef (StructDef ("PerfGroupRead", [ + ("count", U32); + ("values", Array (I64, 16)); + ("ids", Array (U64, 16)); + ("time_enabled", U64); + ("time_running", U64); + ], builtin_pos)); ] (** Default field values for structs that support partial initialisation. @@ -372,13 +429,20 @@ let builtin_types = [ let get_struct_field_defaults = function | "perf_options" -> Some [ - ("pid", IntLit (Signed64 (-1L), None)); - ("cpu", IntLit (Signed64 0L, None)); - ("period", IntLit (Unsigned64 1000000L, None)); - ("wakeup", IntLit (Unsigned64 1L, None)); - ("inherit", BoolLit false); - ("exclude_kernel", BoolLit false); - ("exclude_user", BoolLit false); + ("pid", Literal (IntLit (Signed64 (-1L), None))); + ("cpu", Literal (IntLit (Signed64 0L, None))); + ("group_fd", Literal (IntLit (Signed64 (-1L), None))); + ("group", StructLiteral ("PerfAttachment", [ + ("perf_fd", make_expr (Literal (IntLit (Signed64 (-1L), None))) builtin_pos); + ("link_id", make_expr (Literal (IntLit (Signed64 (-1L), None))) builtin_pos); + ("prog_fd", make_expr (Literal (IntLit (Signed64 (-1L), None))) builtin_pos); + ("generation", make_expr (Literal (IntLit (Unsigned64 0L, None))) builtin_pos); + ])); + ("period", Literal (IntLit (Unsigned64 1000000L, None))); + ("wakeup", Literal (IntLit (Unsigned64 1L, None))); + ("inherit", Literal (BoolLit false)); + ("exclude_kernel", Literal (BoolLit false)); + ("exclude_user", Literal (BoolLit false)); ] | _ -> None diff --git a/src/type_checker.ml b/src/type_checker.ml index e46c035..b3f288e 100644 --- a/src/type_checker.ml +++ b/src/type_checker.ml @@ -205,6 +205,216 @@ let loop_depth = ref 0 (** Helper to create type error *) let type_error msg pos = raise (Type_error (msg, pos)) +let getenv_opt name = + try Some (Sys.getenv name) with Not_found -> None + +let hashtbl_find_opt tbl key = + try Some (Hashtbl.find tbl key) with Not_found -> None + +let parse_positive_int text = + try + let value = int_of_string (String.trim text) in + if value > 0 then Some value else None + with _ -> None + +let read_positive_int_file path = + try + let ic = open_in path in + Fun.protect + ~finally:(fun () -> close_in_noerr ic) + (fun () -> parse_positive_int (input_line ic)) + with _ -> None + +let detected_perf_group_max_events () = + match getenv_opt "KERNELSCRIPT_PERF_GROUP_MAX_EVENTS" with + | Some value -> + (match parse_positive_int value with + | Some parsed -> parsed + | None -> 4) + | None -> + (* Some PMU drivers expose a counter count in sysfs; many do not. + Use a conservative 4-counter fallback, matching common generic PMUs. *) + let candidate_files = [ + "/sys/bus/event_source/devices/cpu/caps/num_counters"; + "/sys/bus/event_source/devices/cpu/caps/num_events"; + ] in + (match List.find_map read_positive_int_file candidate_files with + | Some detected -> detected + | None -> 4) + +type perf_attach_group_ref = { + perf_attach_name: string; + perf_attach_leader: string option; + perf_attach_pmu_slots: int; + perf_attach_pos: position; +} + +let is_attach_to_perf_options expr = + match expr.expr_desc with + | Call ({ expr_desc = Identifier "attach"; _ }, [_prog; opts; _flags]) -> + (match opts.expr_desc with + | StructLiteral ("perf_options", _) -> true + | _ -> false) + | _ -> false + +let perf_options_static_group_leader expr = + match expr.expr_desc with + | StructLiteral ("perf_options", fields) -> + let group_leader = + match List.assoc_opt "group" fields with + | Some { expr_desc = Identifier leader; _ } -> Some leader + | _ -> None + in + (match group_leader with + | Some _ -> group_leader + | None -> + (match List.assoc_opt "group_fd" fields with + | Some { expr_desc = FieldAccess ({ expr_desc = Identifier leader; _ }, "perf_fd"); _ } -> + Some leader + | _ -> None)) + | _ -> None + +let perf_type_consumes_pmu_slot expr = + match expr.expr_desc with + | Identifier "perf_type_software" + | Identifier "perf_type_tracepoint" -> 0 + | Literal (IntLit (Signed64 value, _)) when value = 1L || value = 2L -> 0 + | Literal (IntLit (Unsigned64 value, _)) when value = 1L || value = 2L -> 0 + | _ -> 1 + +let perf_options_pmu_slots expr = + match expr.expr_desc with + | StructLiteral ("perf_options", fields) -> + (match List.assoc_opt "perf_type" fields with + | Some perf_type_expr -> perf_type_consumes_pmu_slot perf_type_expr + | None -> 1) + | _ -> 1 + +let perf_attach_decl_from_expr name expr pos = + match expr.expr_desc with + | Call ({ expr_desc = Identifier "attach"; _ }, [_prog; opts; _flags]) + when is_attach_to_perf_options expr -> + Some { + perf_attach_name = name; + perf_attach_leader = perf_options_static_group_leader opts; + perf_attach_pmu_slots = perf_options_pmu_slots opts; + perf_attach_pos = pos; + } + | _ -> None + +let validate_static_perf_event_groups_in_function func = + let collect_expr acc _expr = acc in + let rec collect_statement acc stmt = + match stmt.stmt_desc with + | Declaration (name, _, Some expr) + | ConstDeclaration (name, _, expr) -> + let acc = + match perf_attach_decl_from_expr name expr stmt.stmt_pos with + | Some attach -> attach :: acc + | None -> acc + in + collect_expr acc expr + | Declaration (_, _, None) -> acc + | ExprStmt expr + | Assignment (_, expr) + | CompoundAssignment (_, _, expr) + | Throw expr + | Defer expr + | FieldAssignment (_, _, expr) + | ArrowAssignment (_, _, expr) + | IndexAssignment (_, _, expr) -> + collect_expr acc expr + | CompoundIndexAssignment (_, _, _, expr) + | CompoundFieldIndexAssignment (_, _, _, _, expr) -> + collect_expr acc expr + | Return (Some expr) -> collect_expr acc expr + | Return None + | Break + | Continue + | Delete _ -> acc + | If (_cond, then_body, else_body) -> + let acc = collect_statements acc then_body in + (match else_body with + | Some body -> collect_statements acc body + | None -> acc) + | IfLet (_, expr, then_body, else_body) -> + let acc = collect_expr acc expr in + let acc = collect_statements acc then_body in + (match else_body with + | Some body -> collect_statements acc body + | None -> acc) + | For (_, start_expr, end_expr, body) -> + collect_statements (collect_expr (collect_expr acc start_expr) end_expr) body + | ForIter (_, _, expr, body) + | While (expr, body) -> + collect_statements (collect_expr acc expr) body + | Try (try_body, catch_clauses) -> + List.fold_left + (fun acc clause -> collect_statements acc clause.catch_body) + (collect_statements acc try_body) + catch_clauses + and collect_statements acc stmts = + List.fold_left collect_statement acc stmts + in + let attachments = collect_statements [] func.func_body |> List.rev in + let attachment_by_name = Hashtbl.create 16 in + let parent_by_name = Hashtbl.create 16 in + List.iter (fun attach -> + Hashtbl.replace attachment_by_name attach.perf_attach_name attach; + match attach.perf_attach_leader with + | Some leader -> Hashtbl.replace parent_by_name attach.perf_attach_name leader + | None -> () + ) attachments; + + let rec root_of seen name pos = + if List.mem name seen then + type_error ("perf event group contains a cycle at attachment '" ^ name ^ "'") pos + else + match hashtbl_find_opt parent_by_name name with + | None -> name + | Some parent -> + (match hashtbl_find_opt parent_by_name parent with + | Some _ -> + let root = root_of (name :: seen) parent pos in + type_error + ("perf event group member '" ^ name ^ "' uses '" ^ parent ^ + "' as its leader, but '" ^ parent ^ + "' is already a group member; use root leader '" ^ root ^ "'") + pos + | None -> parent) + in + + let max_group_events = detected_perf_group_max_events () in + let counts = Hashtbl.create 8 in + let positions = Hashtbl.create 8 in + List.iter (fun attach -> + let root = + match attach.perf_attach_leader with + | None -> attach.perf_attach_name + | Some _ -> root_of [] attach.perf_attach_name attach.perf_attach_pos + in + if Hashtbl.mem attachment_by_name root then ( + Hashtbl.replace counts root (attach.perf_attach_pmu_slots + Option.value ~default:0 (hashtbl_find_opt counts root)); + if not (Hashtbl.mem positions root) then + Hashtbl.replace positions root attach.perf_attach_pos + ) + ) attachments; + Hashtbl.iter (fun root count -> + if count > max_group_events then + let pos = Option.value ~default:func.func_pos (hashtbl_find_opt positions root) in + type_error + (Printf.sprintf + "perf event group rooted at '%s' needs %d PMU counter slot(s), but target PMU group limit is %d; split the events into separate groups or reduce the group size" + root count max_group_events) + pos + ) counts + +let validate_static_perf_event_groups ast = + List.iter (function + | GlobalFunction func -> validate_static_perf_event_groups_in_function func + | _ -> () + ) ast + (** Validate void function usage in expression context *) let validate_void_in_expression expr_type func_name context pos = match expr_type, context with @@ -411,6 +621,12 @@ let builtin_return_type_for_call name arg_types default_return_type = Void | "read", _ -> I64 + | "read_raw", _ -> + I64 + | "read_details", _ -> + Struct "PerfReadDetails" + | "read_group", _ -> + Struct "PerfGroupRead" | _ -> default_return_type @@ -1190,9 +1406,9 @@ and type_check_struct_literal ctx struct_name field_assignments pos = match Stdlib.get_struct_field_defaults struct_name with | None -> field_assignments | Some defaults -> - List.fold_left (fun acc (field_name, default_lit) -> + List.fold_left (fun acc (field_name, default_expr_desc) -> if List.mem_assoc field_name acc then acc - else acc @ [(field_name, make_expr (Literal default_lit) pos)] + else acc @ [(field_name, make_expr default_expr_desc pos)] ) field_assignments defaults in (* Type check each field assignment *) @@ -3071,6 +3287,8 @@ let rec type_check_and_annotate_ast ?symbol_table:(provided_symbol_table=None) ? let _ = include_decl in (* Suppress unused variable warning *) () ) ast; + + validate_static_perf_event_groups ast; (* Second pass: type check attributed functions and global functions with multi-program awareness *) let (typed_attributed_functions, typed_userspace_functions) = List.fold_left (fun (attr_acc, userspace_acc) decl -> @@ -3575,5 +3793,3 @@ and populate_multi_program_context ast multi_prog_analysis = | other_decl -> other_decl ) ast - - diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 3afb3b2..e3a68db 100644 --- a/src/userspace_codegen.ml +++ b/src/userspace_codegen.ml @@ -384,6 +384,9 @@ type function_usage = { mutable uses_attach: bool; mutable uses_attach_perf: bool; mutable uses_perf_read: bool; + mutable uses_perf_read_raw: bool; + mutable uses_perf_read_details: bool; + mutable uses_perf_read_group: bool; mutable uses_detach: bool; mutable uses_map_operations: bool; mutable uses_daemon: bool; @@ -397,6 +400,9 @@ let create_function_usage () = { uses_attach = false; uses_attach_perf = false; uses_perf_read = false; + uses_perf_read_raw = false; + uses_perf_read_details = false; + uses_perf_read_group = false; uses_detach = false; uses_map_operations = false; uses_daemon = false; @@ -715,6 +721,12 @@ let track_function_usage ctx instr = ctx.function_usage.uses_attach <- true) | "read" -> ctx.function_usage.uses_perf_read <- true + | "read_raw" -> + ctx.function_usage.uses_perf_read_raw <- true + | "read_details" -> + ctx.function_usage.uses_perf_read_details <- true + | "read_group" -> + ctx.function_usage.uses_perf_read_group <- true | "detach" -> ctx.function_usage.uses_detach <- true | "daemon" -> ctx.function_usage.uses_daemon <- true | "exec" -> @@ -1883,7 +1895,11 @@ let rec generate_c_instruction_from_ir ctx instruction = (match init_expr_opt with | Some init_expr -> let init_str = generate_c_expression_from_ir ctx init_expr in - sprintf "%s = %s;" array_decl init_str + (match init_expr.expr_desc with + | IRValue { value_desc = IRLiteral (ArrayLit _); _ } -> + sprintf "%s = %s;" array_decl init_str + | _ -> + sprintf "%s;\n memcpy(%s, %s, sizeof(%s));" array_decl c_var_name init_str c_var_name) | None -> sprintf "%s;" array_decl) | _ -> @@ -2034,6 +2050,21 @@ let rec generate_c_instruction_from_ir ctx instruction = (match c_args with | [attachment] -> ("ks_perf_attachment_read", [attachment]) | _ -> failwith "read expects exactly one argument") + | "read_raw" -> + ctx.function_usage.uses_perf_read_raw <- true; + (match c_args with + | [attachment] -> ("ks_perf_attachment_read_raw", [attachment]) + | _ -> failwith "read_raw expects exactly one argument") + | "read_details" -> + ctx.function_usage.uses_perf_read_details <- true; + (match c_args with + | [attachment] -> ("ks_perf_attachment_read_details", [attachment]) + | _ -> failwith "read_details expects exactly one argument") + | "read_group" -> + ctx.function_usage.uses_perf_read_group <- true; + (match c_args with + | [attachment] -> ("ks_perf_attachment_read_group", [attachment]) + | _ -> failwith "read_group expects exactly one argument") | _ -> (userspace_impl, c_args)) | None -> (* Regular function call *) @@ -2367,8 +2398,33 @@ let collect_undeclared_variables_in_function ir_func = | _ -> () in + let rec collect_declared_from_instr ir_instr = + collect_declared_vars ir_instr; + match ir_instr.instr_desc with + | IRIf (_, then_instrs, else_instrs_opt) -> + List.iter collect_declared_from_instr then_instrs; + (match else_instrs_opt with + | Some else_instrs -> List.iter collect_declared_from_instr else_instrs + | None -> ()) + | IRIfElseChain (conditions_and_bodies, final_else) -> + List.iter (fun (_, instrs) -> + List.iter collect_declared_from_instr instrs + ) conditions_and_bodies; + (match final_else with + | Some instrs -> List.iter collect_declared_from_instr instrs + | None -> ()) + | IRBpfLoop (_, _, _, _, body_instructions) -> + List.iter collect_declared_from_instr body_instructions + | IRTry (try_instrs, catch_clauses) -> + List.iter collect_declared_from_instr try_instrs; + List.iter (fun clause -> + List.iter collect_declared_from_instr clause.catch_body + ) catch_clauses + | _ -> () + in + let collect_declared_from_instrs instrs = - List.iter collect_declared_vars instrs + List.iter collect_declared_from_instr instrs in List.iter (fun block -> @@ -2587,6 +2643,10 @@ let generate_c_function_from_ir ?(global_variables = []) ?(base_name = "") ?(con let rec collect_declared_vars ir_instr = match ir_instr.instr_desc with | IRVariableDecl (dest_val, _, _) -> + (match dest_val.value_desc with + | IRVariable var_name | IRTempVariable var_name -> + Hashtbl.replace ctx.declared_via_ir var_name () + | _ -> ()); (* Only user variables (IRVariable) need var_ prefix, not compiler temps (IRTempVariable) *) (match dest_val.value_desc with | IRVariable var_name -> @@ -3793,6 +3853,9 @@ let generate_complete_userspace_program_from_ir ?(config_declarations = []) ?(ta uses_attach = acc_usage.uses_attach || func_usage.uses_attach; uses_attach_perf = acc_usage.uses_attach_perf || func_usage.uses_attach_perf; uses_perf_read = acc_usage.uses_perf_read || func_usage.uses_perf_read; + uses_perf_read_raw = acc_usage.uses_perf_read_raw || func_usage.uses_perf_read_raw; + uses_perf_read_details = acc_usage.uses_perf_read_details || func_usage.uses_perf_read_details; + uses_perf_read_group = acc_usage.uses_perf_read_group || func_usage.uses_perf_read_group; uses_detach = acc_usage.uses_detach || func_usage.uses_detach; uses_map_operations = acc_usage.uses_map_operations || func_usage.uses_map_operations; uses_daemon = acc_usage.uses_daemon || func_usage.uses_daemon; @@ -3828,7 +3891,13 @@ let generate_complete_userspace_program_from_ir ?(config_declarations = []) ?(ta (* For header generation, use all global maps if there are pinned maps, otherwise use the filtered list *) let maps_for_headers = if has_any_pinned_maps then global_maps else used_global_maps_with_exec in - let uses_bpf_functions = all_usage.uses_load || all_usage.uses_attach || all_usage.uses_detach || all_usage.uses_attach_perf || all_usage.uses_perf_read in + let uses_any_perf_read = + all_usage.uses_perf_read || + all_usage.uses_perf_read_raw || + all_usage.uses_perf_read_details || + all_usage.uses_perf_read_group + in + let uses_bpf_functions = all_usage.uses_load || all_usage.uses_attach || all_usage.uses_detach || all_usage.uses_attach_perf || uses_any_perf_read in let base_includes = generate_headers_for_maps ~uses_bpf_functions maps_for_headers in let bpf_attach_includes = if uses_bpf_functions then "#include \n" @@ -3902,6 +3971,29 @@ typedef enum { cpu_migrations = PERF_COUNT_SW_CPU_MIGRATIONS } perf_sw_config; +typedef struct PerfAttachment { + int perf_fd; + int link_id; + int prog_fd; + uint64_t generation; +} PerfAttachment; + +typedef struct PerfReadDetails { + int64_t raw; + int64_t scaled; + uint64_t time_enabled; + uint64_t time_running; +} PerfReadDetails; + +#define KS_PERF_GROUP_MAX_VALUES 16 +typedef struct PerfGroupRead { + uint32_t count; + int64_t values[KS_PERF_GROUP_MAX_VALUES]; + uint64_t ids[KS_PERF_GROUP_MAX_VALUES]; + uint64_t time_enabled; + uint64_t time_running; +} PerfGroupRead; + /* ks_perf_options holds all KernelScript perf_options fields plus the inner * kernel perf_event_attr (from linux/perf_event.h) that ks_open_perf_event fills. */ typedef struct { @@ -3910,6 +4002,8 @@ typedef struct { uint64_t perf_config; /* perf_event_attr.config value for the chosen type */ int32_t pid; /* process ID (-1 = all processes, default) */ int32_t cpu; /* CPU number (0 = CPU 0, default) */ + int32_t group_fd; /* perf event group leader fd (-1 = no group, default) */ + PerfAttachment group; /* high-level group leader attachment */ uint64_t period; /* sampling period (default 1 000 000) */ uint32_t wakeup; /* wakeup after N events (default 1) */ bool inherit; /* inherit to child processes (default false) */ @@ -4114,16 +4208,8 @@ void cleanup_bpf_maps(void) { let load_function = generate_load_function_with_tail_calls base_name all_usage tail_call_analysis all_setup_code kfunc_dependencies (Ir.get_global_variables ir_multi_prog) in (* Global attachment storage (generated when attach/detach/perf attach/perf read are used) *) - let uses_perf_state = all_usage.uses_attach_perf || all_usage.uses_perf_read in - let perf_typedef = if uses_perf_state then - {|typedef struct PerfAttachment { - int perf_fd; - int link_id; - int prog_fd; - uint64_t generation; -} PerfAttachment; -|} - else "" in + let uses_perf_state = all_usage.uses_attach_perf || uses_any_perf_read in + let perf_typedef = "" in let perf_state_decls = if uses_perf_state then {| struct perf_attachment_state { _Atomic uint64_t generation; @@ -4275,6 +4361,82 @@ void cleanup_bpf_maps(void) { } return NULL; } + + static int perf_group_has_active_members_locked(struct attachment_entry *leader) { + if (!leader || + leader->type != BPF_PROG_TYPE_PERF_EVENT || + leader->perf_fd < 0 || + leader->is_group_member) { + return 0; + } + + struct attachment_entry *entry = attached_programs; + while (entry) { + if (entry != leader && + entry->type == BPF_PROG_TYPE_PERF_EVENT && + entry->is_group_member && + entry->group_leader_fd == leader->perf_fd && + !entry->detaching) { + return 1; + } + entry = entry->next; + } + return 0; + } + + static struct attachment_entry *mark_next_perf_group_member_detaching_locked(struct attachment_entry *leader) { + if (!leader || + leader->type != BPF_PROG_TYPE_PERF_EVENT || + leader->perf_fd < 0 || + leader->is_group_member) { + return NULL; + } + + struct attachment_entry *entry = attached_programs; + while (entry) { + if (entry != leader && + entry->type == BPF_PROG_TYPE_PERF_EVENT && + entry->is_group_member && + entry->group_leader_fd == leader->perf_fd && + !entry->detaching) { + entry->detaching = 1; + invalidate_perf_attachment_state_locked(entry); + return entry; + } + entry = entry->next; + } + return NULL; + } + + static int perf_mark_group_members_detaching_locked(struct attachment_entry *leader) { + int count = 0; + while (mark_next_perf_group_member_detaching_locked(leader) != NULL) { + count++; + } + return count; + } + + static struct attachment_entry *find_marked_perf_group_member_locked(struct attachment_entry *leader) { + if (!leader || + leader->type != BPF_PROG_TYPE_PERF_EVENT || + leader->perf_fd < 0 || + leader->is_group_member) { + return NULL; + } + + struct attachment_entry *entry = attached_programs; + while (entry) { + if (entry != leader && + entry->type == BPF_PROG_TYPE_PERF_EVENT && + entry->is_group_member && + entry->group_leader_fd == leader->perf_fd && + entry->detaching) { + return entry; + } + entry = entry->next; + } + return NULL; + } |} else "" in let attachment_storage = if all_usage.uses_attach || all_usage.uses_detach || uses_perf_state then @@ -4288,6 +4450,8 @@ void cleanup_bpf_maps(void) { struct bpf_link *link; // For kprobe/tracepoint programs (NULL for XDP) int ifindex; // For XDP programs (0 for kprobe/tracepoint) int perf_fd; // For perf_event programs (-1 otherwise) + int group_leader_fd; // Perf group leader fd for members (-1 otherwise) + int is_group_member; // Non-zero when perf_fd belongs to a group leader int detaching; // Non-zero while teardown is in progress uint64_t generation; // PerfAttachment stale-handle token enum bpf_prog_type type; @@ -4303,6 +4467,7 @@ void cleanup_bpf_maps(void) { // Duplicate check is performed atomically under the same lock as insertion. static int add_attachment(int prog_fd, const char *target, uint32_t flags, struct bpf_link *link, int ifindex, int perf_fd, + int group_leader_fd, int is_group_member, enum bpf_prog_type type, int *attachment_id_out, uint64_t *generation_out) { struct attachment_entry *entry = malloc(sizeof(struct attachment_entry)); @@ -4319,6 +4484,8 @@ void cleanup_bpf_maps(void) { entry->link = link; entry->ifindex = ifindex; entry->perf_fd = perf_fd; + entry->group_leader_fd = group_leader_fd; + entry->is_group_member = is_group_member; entry->type = type; entry->detaching = 0; @@ -4401,7 +4568,7 @@ void cleanup_bpf_maps(void) { } // Store XDP attachment (no bpf_link for XDP) - if (add_attachment(prog_fd, target, flags, NULL, ifindex, -1, BPF_PROG_TYPE_XDP, NULL, NULL) != 0) { + if (add_attachment(prog_fd, target, flags, NULL, ifindex, -1, -1, 0, BPF_PROG_TYPE_XDP, NULL, NULL) != 0) { // If storage fails, detach and return error bpf_xdp_detach(ifindex, flags, NULL); return -1; @@ -4431,7 +4598,7 @@ void cleanup_bpf_maps(void) { printf("Kprobe attached to function: %s\n", target); // Store probe attachment for later cleanup - if (add_attachment(prog_fd, target, flags, link, 0, -1, BPF_PROG_TYPE_KPROBE, NULL, NULL) != 0) { + if (add_attachment(prog_fd, target, flags, link, 0, -1, -1, 0, BPF_PROG_TYPE_KPROBE, NULL, NULL) != 0) { // If storage fails, destroy link and return error bpf_link__destroy(link); return -1; @@ -4460,7 +4627,7 @@ void cleanup_bpf_maps(void) { printf("Fentry/fexit program attached to function: %s\n", target); // Store tracing attachment for later cleanup - if (add_attachment(prog_fd, target, flags, link, 0, -1, BPF_PROG_TYPE_TRACING, NULL, NULL) != 0) { + if (add_attachment(prog_fd, target, flags, link, 0, -1, -1, 0, BPF_PROG_TYPE_TRACING, NULL, NULL) != 0) { // If storage fails, destroy link and return error bpf_link__destroy(link); return -1; @@ -4504,7 +4671,7 @@ void cleanup_bpf_maps(void) { } // Store tracepoint attachment for later cleanup - if (add_attachment(prog_fd, target, flags, link, 0, -1, BPF_PROG_TYPE_TRACEPOINT, NULL, NULL) != 0) { + if (add_attachment(prog_fd, target, flags, link, 0, -1, -1, 0, BPF_PROG_TYPE_TRACEPOINT, NULL, NULL) != 0) { // If storage fails, destroy link and return error bpf_link__destroy(link); return -1; @@ -4541,7 +4708,7 @@ void cleanup_bpf_maps(void) { } // Store TC attachment for later cleanup (flags no longer needed for direction) - if (add_attachment(prog_fd, target, 0, link, ifindex, -1, BPF_PROG_TYPE_SCHED_CLS, NULL, NULL) != 0) { + if (add_attachment(prog_fd, target, 0, link, ifindex, -1, -1, 0, BPF_PROG_TYPE_SCHED_CLS, NULL, NULL) != 0) { // If storage fails, destroy link and return error bpf_link__destroy(link); return -1; @@ -4579,6 +4746,22 @@ void cleanup_bpf_maps(void) { let invalidate_call_line = if uses_perf_state then " invalidate_perf_attachment_state_locked(entry);\n" else "" in + let perf_leader_guard_line = if uses_perf_state then + {| if (entry->type == BPF_PROG_TYPE_PERF_EVENT && + !entry->is_group_member && + perf_group_has_active_members_locked(entry)) { + int cascade_count = perf_mark_group_members_detaching_locked(entry); + fprintf(stderr, + "Detaching perf group leader fd %d cascades to %d active member(s)\n", + entry->perf_fd, cascade_count); + struct attachment_entry *member = find_marked_perf_group_member_locked(entry); + if (member) { + entry = member; + break; + } + } +|} + else "" in let detach_entry_dispatch = if all_usage.uses_detach || all_usage.uses_attach_perf then sprintf {|static void ks_detach_attachment_entry(struct attachment_entry *entry, int identifier_for_logs) { if (!entry) { @@ -4651,8 +4834,13 @@ void cleanup_bpf_maps(void) { pthread_mutex_lock(&attachment_mutex); struct attachment_entry *entry = attached_programs; while (entry) { + if (entry->type == BPF_PROG_TYPE_PERF_EVENT && + entry->is_group_member && + entry->detaching) { + break; + } if (entry->prog_fd == prog_fd && !entry->detaching) { - entry->detaching = 1; +%s entry->detaching = 1; %s break; } entry = entry->next; @@ -4678,7 +4866,7 @@ void cleanup_bpf_maps(void) { pthread_mutex_unlock(&attachment_mutex); free(entry); } -}|} invalidate_call_line +}|} perf_leader_guard_line invalidate_call_line else "" in let perf_detach_function = if all_usage.uses_attach_perf then {|void ks_detach_perf_attachment(PerfAttachment attachment) { @@ -4690,6 +4878,12 @@ void cleanup_bpf_maps(void) { pthread_mutex_lock(&attachment_mutex); struct attachment_entry *entry = find_attachment_by_id_locked(attachment.link_id); if (entry && !entry->detaching) { + if (!entry->is_group_member && perf_group_has_active_members_locked(entry)) { + int cascade_count = perf_mark_group_members_detaching_locked(entry); + fprintf(stderr, + "Detaching perf group leader fd %d cascades to %d active member(s)\n", + entry->perf_fd, cascade_count); + } entry->detaching = 1; invalidate_perf_attachment_state_locked(entry); } else { @@ -4702,6 +4896,32 @@ void cleanup_bpf_maps(void) { return; } + while (1) { + pthread_mutex_lock(&attachment_mutex); + struct attachment_entry *member = find_marked_perf_group_member_locked(entry); + if (!member) { + member = mark_next_perf_group_member_detaching_locked(entry); + } + pthread_mutex_unlock(&attachment_mutex); + if (!member) { + break; + } + + ks_detach_attachment_entry(member, member->attachment_id); + + pthread_mutex_lock(&attachment_mutex); + struct attachment_entry **member_cur = &attached_programs; + while (*member_cur) { + if (*member_cur == member) { + *member_cur = member->next; + break; + } + member_cur = &(*member_cur)->next; + } + pthread_mutex_unlock(&attachment_mutex); + free(member); + } + ks_detach_attachment_entry(entry, attachment.link_id); pthread_mutex_lock(&attachment_mutex); @@ -4834,7 +5054,7 @@ static int ensure_bpf_dir(const char *path) { else "" in let perf_attach_function = if all_usage.uses_attach_perf then - {|int ks_open_perf_event(ks_perf_options ks_attr) { + let perf_attach_template = {|int ks_open_perf_event(ks_perf_options ks_attr) { /* Fill the BTF-derived struct perf_event_attr from KernelScript fields */ ks_attr.attr.type = (__u32)ks_attr.perf_type; ks_attr.attr.size = sizeof(struct perf_event_attr); @@ -4842,6 +5062,9 @@ static int ensure_bpf_dir(const char *path) { ks_attr.attr.sample_type = 0; ks_attr.attr.sample_period = ks_attr.period > 0 ? ks_attr.period : 1000000; ks_attr.attr.wakeup_events = ks_attr.wakeup > 0 ? ks_attr.wakeup : 1; + ks_attr.attr.read_format = + PERF_FORMAT_TOTAL_TIME_ENABLED | + PERF_FORMAT_TOTAL_TIME_RUNNING; ks_attr.attr.inherit = ks_attr.inherit ? 1 : 0; ks_attr.attr.exclude_kernel = ks_attr.exclude_kernel ? 1 : 0; ks_attr.attr.exclude_user = ks_attr.exclude_user ? 1 : 0; @@ -4849,6 +5072,12 @@ static int ensure_bpf_dir(const char *path) { int cpu = ks_attr.cpu; int pid = ks_attr.pid; + int group_fd = ks_attr.group_fd; + if (ks_attr.group.perf_fd >= 0 && + ks_attr.group.link_id > 0 && + ks_attr.group.generation != 0) { + group_fd = ks_attr.group.perf_fd; + } if (pid < -1) { fprintf(stderr, "ks_open_perf_event: invalid pid %d (expected >= -1)\n", pid); @@ -4858,21 +5087,54 @@ static int ensure_bpf_dir(const char *path) { fprintf(stderr, "ks_open_perf_event: invalid cpu %d (expected >= -1)\n", cpu); return -1; } + if (group_fd < -1) { + fprintf(stderr, "ks_open_perf_event: invalid group_fd %d (expected -1 or a leader fd >= 0)\n", group_fd); + return -1; + } + if (ks_attr.group.perf_fd < -1) { + fprintf(stderr, "ks_open_perf_event: invalid group leader attachment fd %d\n", ks_attr.group.perf_fd); + return -1; + } if (pid == -1 && cpu == -1) { fprintf(stderr, "ks_open_perf_event: system-wide perf events require an explicit cpu >= 0\n"); return -1; } - int perf_fd = (int)syscall(SYS_perf_event_open, &ks_attr.attr, pid, cpu, -1, PERF_FLAG_FD_CLOEXEC); + int perf_fd = (int)syscall(SYS_perf_event_open, &ks_attr.attr, pid, cpu, group_fd, PERF_FLAG_FD_CLOEXEC); if (perf_fd < 0) { - fprintf(stderr, "ks_open_perf_event: perf_event_open failed: %s\n", strerror(errno)); + fprintf(stderr, "ks_open_perf_event: perf_event_open failed for group_fd %d: %s\n", + group_fd, strerror(errno)); return -1; } return perf_fd; } +static int ks_restart_perf_group(int group_fd) { + if (group_fd < 0) { + fprintf(stderr, "ks_restart_perf_group: invalid group leader fd %d\n", group_fd); + return -1; + } + if (ioctl(group_fd, PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP) != 0) { + fprintf(stderr, "Failed to disable perf event group leader fd %d: %s\n", + group_fd, strerror(errno)); + return -1; + } + if (ioctl(group_fd, PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP) != 0) { + fprintf(stderr, "Failed to reset perf event group leader fd %d: %s\n", + group_fd, strerror(errno)); + return -1; + } + if (ioctl(group_fd, PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP) != 0) { + fprintf(stderr, "Failed to enable perf event group leader fd %d: %s\n", + group_fd, strerror(errno)); + return -1; + } + return 0; +} + /* Attach a perf_event BPF program using a ks_perf_options config. - * Opens the perf fd, resets, attaches, and enables counting in one step. */ + * Standalone events are reset and enabled directly; group members restart their + * leader group after the member link is attached. */ PerfAttachment ks_attach_perf_event(int prog_fd, ks_perf_options opts, int flags) { PerfAttachment attachment = { .perf_fd = -1, @@ -4899,6 +5161,11 @@ PerfAttachment ks_attach_perf_event(int prog_fd, ks_perf_options opts, int flags return attachment; } + int effective_group_fd = + (opts.group.perf_fd >= 0 && opts.group.link_id > 0 && opts.group.generation != 0) + ? opts.group.perf_fd + : opts.group_fd; + bool is_group_member = effective_group_fd >= 0; int perf_fd = ks_open_perf_event(opts); if (perf_fd < 0) return attachment; @@ -4909,7 +5176,7 @@ PerfAttachment ks_attach_perf_event(int prog_fd, ks_perf_options opts, int flags return attachment; } - if (ioctl(perf_fd, PERF_EVENT_IOC_RESET, 0) != 0) { + if (!is_group_member && ioctl(perf_fd, PERF_EVENT_IOC_RESET, 0) != 0) { fprintf(stderr, "Failed to reset perf event fd %d: %s\n", perf_fd, strerror(errno)); close(perf_fd); return attachment; @@ -4923,7 +5190,15 @@ PerfAttachment ks_attach_perf_event(int prog_fd, ks_perf_options opts, int flags return attachment; } - if (ioctl(perf_fd, PERF_EVENT_IOC_ENABLE, 0) != 0) { + if (is_group_member) { + if (ks_restart_perf_group(effective_group_fd) != 0) { + fprintf(stderr, "Failed to restart perf event group for member fd %d leader fd %d\n", + perf_fd, effective_group_fd); + bpf_link__destroy(link); + close(perf_fd); + return attachment; + } + } else if (ioctl(perf_fd, PERF_EVENT_IOC_ENABLE, 0) != 0) { fprintf(stderr, "Failed to enable perf event fd %d: %s\n", perf_fd, strerror(errno)); bpf_link__destroy(link); close(perf_fd); @@ -4932,14 +5207,16 @@ PerfAttachment ks_attach_perf_event(int prog_fd, ks_perf_options opts, int flags char perf_target[128]; snprintf(perf_target, sizeof(perf_target), - "perf_event:type=%d config=%llu period=%llu", + "perf_event:type=%d config=%llu period=%llu group_fd=%d", opts.perf_type, (unsigned long long)opts.perf_config, - (unsigned long long)opts.period); + (unsigned long long)opts.period, + effective_group_fd); int attachment_id = -1; uint64_t generation = 0; if (add_attachment(prog_fd, perf_target, (uint32_t)flags, link, 0, perf_fd, + effective_group_fd, is_group_member ? 1 : 0, BPF_PROG_TYPE_PERF_EVENT, &attachment_id, &generation) != 0) { ioctl(perf_fd, PERF_EVENT_IOC_DISABLE, 0); bpf_link__destroy(link); @@ -4964,29 +5241,111 @@ PerfAttachment ks_attach_perf_event(int prog_fd, ks_perf_options opts, int flags return attachment; } |} + in + if all_usage.uses_perf_read_group then + Str.global_replace + (Str.regexp_string " PERF_FORMAT_TOTAL_TIME_ENABLED |\n PERF_FORMAT_TOTAL_TIME_RUNNING;") + " PERF_FORMAT_TOTAL_TIME_ENABLED |\n PERF_FORMAT_TOTAL_TIME_RUNNING |\n PERF_FORMAT_ID |\n PERF_FORMAT_GROUP;" + perf_attach_template + else perf_attach_template else "" in - let perf_read_function = if all_usage.uses_perf_read then - {|/* Read the current hardware counter value from an open perf_fd. - * Returns the raw 64-bit count, or -1 on error. */ -int64_t ks_read_perf_count(int perf_fd) { + let perf_read_function = if uses_any_perf_read then + {|struct ks_perf_read_value { + uint64_t value; + uint64_t time_enabled; + uint64_t time_running; +}; + +struct ks_perf_group_read_value { + uint64_t value; + uint64_t id; +}; + +struct ks_perf_group_read_buffer { + uint64_t nr; + uint64_t time_enabled; + uint64_t time_running; + struct ks_perf_group_read_value values[KS_PERF_GROUP_MAX_VALUES]; +}; + +static int64_t ks_scale_perf_count(uint64_t value, uint64_t time_enabled, uint64_t time_running, const char *caller, int perf_fd) { + if (time_running == 0) { + fprintf(stderr, "%s: perf event fd %d has time_running=0\n", caller, perf_fd); + return -1; + } + if (time_enabled == time_running) { + return (int64_t)value; + } + __uint128_t scaled = + ((__uint128_t)value * (__uint128_t)time_enabled) / time_running; + return (int64_t)scaled; +} + +static int ks_read_perf_details_from_fd(int perf_fd, PerfReadDetails *details, const char *caller) { if (perf_fd < 0) { - fprintf(stderr, "ks_read_perf_count: invalid perf_fd %d\n", perf_fd); + fprintf(stderr, "%s: invalid perf_fd %d\n", caller, perf_fd); + return -1; + } + if (!details) { + fprintf(stderr, "%s: NULL details output\n", caller); return -1; } - uint64_t count = 0; - ssize_t n = read(perf_fd, &count, sizeof(count)); + + struct ks_perf_group_read_buffer group = {0}; + ssize_t n = read(perf_fd, &group, sizeof(group)); if (n < 0) { - fprintf(stderr, "ks_read_perf_count: read failed on perf_fd %d: %s\n", + fprintf(stderr, "%s: read failed on perf_fd %d: %s\n", + caller, perf_fd, strerror(errno)); return -1; } - if (n != sizeof(count)) { - fprintf(stderr, "ks_read_perf_count: short read (%zd bytes) on perf_fd %d\n", - n, perf_fd); + uint64_t value = 0; + uint64_t time_enabled = 0; + uint64_t time_running = 0; + if (n == (ssize_t)sizeof(struct ks_perf_read_value)) { + struct ks_perf_read_value *count = (struct ks_perf_read_value *)&group; + value = count->value; + time_enabled = count->time_enabled; + time_running = count->time_running; + } else if (n >= (ssize_t)(sizeof(uint64_t) * 3 + sizeof(struct ks_perf_group_read_value))) { + if (group.nr == 0) { + fprintf(stderr, "%s: group read returned zero values on perf_fd %d\n", caller, perf_fd); + return -1; + } + value = group.values[0].value; + time_enabled = group.time_enabled; + time_running = group.time_running; + } else { + fprintf(stderr, "%s: short read (%zd bytes) on perf_fd %d\n", + caller, n, perf_fd); + return -1; + } + + details->raw = (int64_t)value; + details->time_enabled = time_enabled; + details->time_running = time_running; + details->scaled = ks_scale_perf_count(value, time_enabled, time_running, caller, perf_fd); + return details->scaled < 0 ? -1 : 0; +} + +/* Read the current raw hardware counter value from an open perf_fd. */ +int64_t ks_read_perf_count_raw(int perf_fd) { + PerfReadDetails details = {0}; + if (ks_read_perf_details_from_fd(perf_fd, &details, "ks_read_perf_count_raw") != 0) { + return -1; + } + return details.raw; +} + +/* Read the current hardware counter value from an open perf_fd. + * Returns a multiplex-scaled count, or -1 on error. */ +int64_t ks_read_perf_count(int perf_fd) { + PerfReadDetails details = {0}; + if (ks_read_perf_details_from_fd(perf_fd, &details, "ks_read_perf_count") != 0) { return -1; } - return (int64_t)count; + return details.scaled; } /* Read the counter for a first-class perf attachment value. */ @@ -5000,6 +5359,101 @@ int64_t ks_perf_attachment_read(PerfAttachment attachment) { perf_attachment_end_read(state); return result; } + +/* Read the raw counter for a first-class perf attachment value. */ +int64_t ks_perf_attachment_read_raw(PerfAttachment attachment) { + struct perf_attachment_state *state = perf_attachment_begin_read(attachment); + if (!state) { + fprintf(stderr, "ks_perf_attachment_read_raw: invalid or stale perf attachment\n"); + return -1; + } + int64_t result = ks_read_perf_count_raw(attachment.perf_fd); + perf_attachment_end_read(state); + return result; +} + +/* Read raw, scaled, and multiplex timing details for a perf attachment. */ +PerfReadDetails ks_perf_attachment_read_details(PerfAttachment attachment) { + PerfReadDetails details = { + .raw = -1, + .scaled = -1, + .time_enabled = 0, + .time_running = 0, + }; + struct perf_attachment_state *state = perf_attachment_begin_read(attachment); + if (!state) { + fprintf(stderr, "ks_perf_attachment_read_details: invalid or stale perf attachment\n"); + return details; + } + (void)ks_read_perf_details_from_fd(attachment.perf_fd, &details, "ks_perf_attachment_read_details"); + perf_attachment_end_read(state); + return details; +} + +/* Read a same-time snapshot from a perf group leader. + * Values are multiplex-scaled individually using the group's timing fields. */ +PerfGroupRead ks_perf_attachment_read_group(PerfAttachment attachment) { + PerfGroupRead result = { + .count = 0, + .time_enabled = 0, + .time_running = 0, + }; + struct perf_attachment_state *state = perf_attachment_begin_read(attachment); + if (!state) { + fprintf(stderr, "ks_perf_attachment_read_group: invalid or stale perf attachment\n"); + return result; + } + + struct ks_perf_group_read_buffer group = {0}; + ssize_t n = read(attachment.perf_fd, &group, sizeof(group)); + if (n < 0) { + fprintf(stderr, "ks_perf_attachment_read_group: read failed on perf_fd %d: %s\n", + attachment.perf_fd, strerror(errno)); + perf_attachment_end_read(state); + return result; + } + if (n < (ssize_t)(sizeof(uint64_t) * 3)) { + fprintf(stderr, "ks_perf_attachment_read_group: short group header read (%zd bytes) on perf_fd %d\n", + n, attachment.perf_fd); + perf_attachment_end_read(state); + return result; + } + + uint64_t available = 0; + size_t header_size = sizeof(uint64_t) * 3; + if ((size_t)n > header_size) { + available = ((size_t)n - header_size) / sizeof(struct ks_perf_group_read_value); + } + uint64_t nr = group.nr; + if (nr > available) { + fprintf(stderr, + "ks_perf_attachment_read_group: short group value read (nr=%llu available=%llu) on perf_fd %d\n", + (unsigned long long)nr, + (unsigned long long)available, + attachment.perf_fd); + nr = available; + } + if (nr > KS_PERF_GROUP_MAX_VALUES) { + fprintf(stderr, + "ks_perf_attachment_read_group: truncating %llu values to %u\n", + (unsigned long long)nr, + KS_PERF_GROUP_MAX_VALUES); + nr = KS_PERF_GROUP_MAX_VALUES; + } + + result.count = (uint32_t)nr; + result.time_enabled = group.time_enabled; + result.time_running = group.time_running; + for (uint32_t i = 0; i < result.count; i++) { + result.ids[i] = group.values[i].id; + result.values[i] = + ks_scale_perf_count(group.values[i].value, group.time_enabled, group.time_running, + "ks_perf_attachment_read_group", attachment.perf_fd); + } + + perf_attachment_end_read(state); + return result; +} |} else "" in diff --git a/tests/dune b/tests/dune index ecb515f..b4e1aab 100644 --- a/tests/dune +++ b/tests/dune @@ -419,7 +419,7 @@ (executable (name test_perf_event_attach) (modules test_perf_event_attach) - (libraries kernelscript alcotest str)) + (libraries kernelscript alcotest test_utils str unix)) (executable (name test_tc) diff --git a/tests/test_perf_event_attach.ml b/tests/test_perf_event_attach.ml index 1e30409..4ab7482 100644 --- a/tests/test_perf_event_attach.ml +++ b/tests/test_perf_event_attach.ml @@ -57,6 +57,7 @@ let perf_attr_expr ~pid ~cpu = ("perf_config", perf_config_value "perf_hw_config" "branch_misses" 5L); ("pid", int32_value pid); ("cpu", int32_value cpu); + ("group_fd", int32_value (-1L)); ("period", uint64_value 1000000L); ("wakeup", uint32_value 1L); ("inherit", bool_value false); @@ -79,6 +80,19 @@ let make_generated_code instructions = let ir_multi_prog = make_ir_multi_program "test" ~userspace_program:userspace_prog test_pos in generate_complete_userspace_program_from_ir userspace_prog [] ir_multi_prog "test.ks" +let make_generated_code_from_source source = + let ast = parse_string source in + let ast_with_builtins = Kernelscript.Stdlib.get_builtin_types () @ ast in + let symbol_table = Kernelscript.Symbol_table.build_symbol_table ast_with_builtins in + let annotated_ast, _typed_programs = + type_check_and_annotate_ast ~symbol_table:(Some symbol_table) ast_with_builtins + in + let ir_multi_prog = Kernelscript.Ir_generator.generate_ir annotated_ast symbol_table "test" in + match ir_multi_prog.userspace_program with + | Some userspace_prog -> + generate_complete_userspace_program_from_ir userspace_prog [] ir_multi_prog "test.ks" + | None -> fail "Expected userspace program in generated IR" + let test_perf_event_codegen_enforces_pid_cpu_rules () = let prog_handle = make_ir_value (IRVariable "prog") IRI32 test_pos in let attr_value = make_ir_value (IRVariable "attr") (IRStruct ("perf_options", [])) test_pos in @@ -140,6 +154,7 @@ let perf_attr_expr_with ~period ~wakeup = ("perf_config", perf_config_value "perf_hw_config" "branch_misses" 5L); ("pid", int32_value 1234L); ("cpu", int32_value 0L); + ("group_fd", int32_value (-1L)); ("period", uint64_value period); ("wakeup", uint32_value wakeup); ("inherit", bool_value false); @@ -195,8 +210,10 @@ let test_perf_event_counting_starts_correctly () = (appears_before code "PERF_EVENT_IOC_RESET" "PERF_EVENT_IOC_ENABLE"); (* 5. BPF program is linked to the perf fd before enabling (attach before enable). *) - check bool "attach_perf_event called before IOC_ENABLE" true - (appears_before code "bpf_program__attach_perf_event" "PERF_EVENT_IOC_ENABLE"); + check bool "attach_perf_event called before standalone IOC_ENABLE" true + (appears_before code + "bpf_program__attach_perf_event(prog, perf_fd)" + "else if (ioctl(perf_fd, PERF_EVENT_IOC_ENABLE, 0) != 0)"); (* 6. Counting truly kicks off: IOC_ENABLE is the last step and must be present. *) check bool "IOC_ENABLE present to start counting" true @@ -223,6 +240,45 @@ let test_perf_event_period_and_wakeup_custom () = check bool "runtime wakeup expression present for custom wakeup" true (contains_substr code "ks_attr.wakeup > 0 ? ks_attr.wakeup : 1") +let test_perf_event_group_fd_codegen () = + let code = make_perf_code_with ~period:1000000L ~wakeup:1L in + check bool "ks_perf_options carries group_fd" true + (contains_substr code "int32_t group_fd;"); + check bool "hand-built perf options default group_fd to -1" true + (contains_substr code ".group_fd = -1"); + check bool "group_fd copied from options" true + (contains_substr code "int group_fd = ks_attr.group_fd;"); + check bool "invalid group_fd rejected" true + (contains_substr code "if (group_fd < -1)"); + check bool "perf_event_open receives variable group_fd" true + (contains_substr code "pid, cpu, group_fd, PERF_FLAG_FD_CLOEXEC"); + check bool "perf_event_open no longer hardcodes no group" false + (contains_substr code "pid, cpu, -1, PERF_FLAG_FD_CLOEXEC"); + check bool "read_format requests multiplex timing" true + (contains_substr code "PERF_FORMAT_TOTAL_TIME_ENABLED" && + contains_substr code "PERF_FORMAT_TOTAL_TIME_RUNNING"); + check bool "group snapshot format omitted until read_group is used" false + (contains_substr code "PERF_FORMAT_GROUP") + +let test_perf_event_group_member_lifecycle_codegen () = + let code = make_perf_code_with ~period:1000000L ~wakeup:1L in + check bool "member branch detected from group_fd" true + (contains_substr code "bool is_group_member = effective_group_fd >= 0;"); + check bool "group restart helper emitted" true + (contains_substr code "static int ks_restart_perf_group(int group_fd)"); + check bool "group disable uses PERF_IOC_FLAG_GROUP" true + (contains_substr code "PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP"); + check bool "group reset uses PERF_IOC_FLAG_GROUP" true + (contains_substr code "PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP"); + check bool "group enable uses PERF_IOC_FLAG_GROUP" true + (contains_substr code "PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP"); + check bool "member restart happens after link attach" true + (appears_before code + "bpf_program__attach_perf_event(prog, perf_fd)" + "ks_restart_perf_group(effective_group_fd)"); + check bool "attachment stores group metadata" true + (contains_substr code "effective_group_fd, is_group_member ? 1 : 0") + let test_standard_attach_uses_libbpf_error_checks () = let prog_handle = make_ir_value (IRVariable "prog") IRI32 test_pos in let target = make_ir_value (IRLiteral (StringLit "eth0")) (IRStr 16) test_pos in @@ -302,6 +358,49 @@ let test_read_helpers_generated_when_used () = check bool "read no longer walks attachment list by link id" false (contains_substr code "struct attachment_entry *cur = find_attachment_by_id_locked(attachment.link_id)") +let test_perf_read_helper_scales_multiplexed_counts () = + let prog_handle = make_ir_value (IRVariable "prog") IRI32 test_pos in + let attr_value = make_ir_value (IRVariable "attr") (IRStruct ("perf_options", [])) test_pos in + let flags_value = uint32_value 0L in + let attachment_value = + make_ir_value + (IRVariable "att") + (IRStruct ("PerfAttachment", [("perf_fd", IRI32); ("link_id", IRI32); ("prog_fd", IRI32); ("generation", IRU64)])) + test_pos + in + let count_value = make_ir_value (IRVariable "count") IRI64 test_pos in + let attr_decl = + make_ir_instruction + (IRVariableDecl (attr_value, IRStruct ("perf_options", []), + Some (perf_attr_expr_with ~period:1000000L ~wakeup:1L))) + test_pos + in + let attach_call = + make_ir_instruction + (IRCall (DirectCall "attach", [prog_handle; attr_value; flags_value], Some attachment_value)) + test_pos + in + let read_call = + make_ir_instruction + (IRCall (DirectCall "read", [attachment_value], Some count_value)) + test_pos + in + let code = make_generated_code [attr_decl; attach_call; read_call] in + check bool "read helper uses timing read struct" true + (contains_substr code "struct ks_perf_read_value"); + check bool "read helper includes time_enabled" true + (contains_substr code "uint64_t time_enabled;"); + check bool "read helper includes time_running" true + (contains_substr code "uint64_t time_running;"); + check bool "time_running zero guard emitted" true + (contains_substr code "if (time_running == 0)"); + check bool "fast path returns raw value" true + (contains_substr code "if (time_enabled == time_running)"); + check bool "scaled path uses 128-bit intermediate" true + (contains_substr code "__uint128_t scaled"); + check bool "scaled path multiplies by time_enabled" true + (contains_substr code "value * (__uint128_t)time_enabled") + let test_perf_attach_event_function_generated () = (* attach(prog, perf_options{...}, 0) must generate ks_attach_perf_event which owns the full open-reset-attach-enable lifecycle in a single C function. *) @@ -393,6 +492,177 @@ let test_detach_attach_concurrent_window () = check bool "detach invalidates stale perf attachment handles before close" true (contains_substr code "invalidate_perf_attachment_state_locked(entry)") +let test_perf_group_source_field_access_codegen () = + let source = {| +@perf_event +fn on_event(ctx: *bpf_perf_event_data) -> i32 { + return 0 +} + +fn main() -> i32 { + var prog = load(on_event) + var cache = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: cache_misses, + }, 0) + var branch = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group_fd: cache.perf_fd, + }, 0) + detach(branch) + detach(cache) + detach(prog) + return 0 +} +|} in + let code = make_generated_code_from_source source in + check bool "source group_fd field access type-checks and codegens" true + (contains_substr code "var_cache.perf_fd"); + check bool "source emits grouped perf option assignment" true + (contains_substr code ".group_fd = __field_access_"); + check bool "leader detach protection helper generated" true + (contains_substr code "perf_group_has_active_members_locked"); + check bool "detach cascades active group leaders" true + (contains_substr code "Detaching perf group leader fd %d cascades to %d active member(s)") + +let test_perf_group_attachment_field_codegen () = + let source = {| +@perf_event +fn on_event(ctx: *bpf_perf_event_data) -> i32 { + return 0 +} + +fn main() -> i32 { + var prog = load(on_event) + var cache = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: cache_misses, + }, 0) + var branch = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group: cache, + }, 0) + detach(branch) + detach(cache) + detach(prog) + return 0 +} +|} in + let code = make_generated_code_from_source source in + check bool "perf_options carries high-level group attachment" true + (contains_substr code "PerfAttachment group;"); + check bool "source group attachment field type-checks and codegens" true + (contains_substr code ".group = var_cache"); + check bool "runtime prefers valid group attachment fd" true + (contains_substr code "opts.group.perf_fd >= 0 && opts.group.link_id > 0 && opts.group.generation != 0") + +let test_perf_read_raw_details_and_group_codegen () = + let source = {| +@perf_event +fn on_event(ctx: *bpf_perf_event_data) -> i32 { + return 0 +} + +fn main() -> i32 { + var prog = load(on_event) + var cache = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: cache_misses, + }, 0) + var branch = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group: cache, + }, 0) + var raw = read_raw(cache) + var details = read_details(cache) + var snapshot = read_group(cache) + print("raw=%lld scaled=%lld group=%u", raw, details.scaled, snapshot.count) + var i = 0 + while (i < snapshot.count) { + print("id=%llu value=%lld", snapshot.ids[i], snapshot.values[i]) + i = i + 1 + } + detach(branch) + detach(cache) + detach(prog) + return 0 +} +|} in + let code = make_generated_code_from_source source in + check bool "raw read helper generated" true + (contains_substr code "ks_perf_attachment_read_raw"); + check bool "details read helper generated" true + (contains_substr code "PerfReadDetails ks_perf_attachment_read_details"); + check bool "group read helper generated" true + (contains_substr code "PerfGroupRead ks_perf_attachment_read_group"); + check bool "group snapshot buffer generated" true + (contains_substr code "struct ks_perf_group_read_buffer"); + check bool "read_group enables group read format" true + (contains_substr code "PERF_FORMAT_ID" && contains_substr code "PERF_FORMAT_GROUP"); + check bool "group values are multiplex scaled" true + (contains_substr code "ks_scale_perf_count(group.values[i].value") + ; + check bool "array field snapshots are copied before indexing" true + (contains_substr code "memcpy(__field_access_"); + check bool "array snapshot indexing dereferences element pointer" true + (contains_substr code "*__array_ptr_") + +let test_perf_group_too_large_static_group_rejected () = + Unix.putenv "KERNELSCRIPT_PERF_GROUP_MAX_EVENTS" "4"; + let source = {| +@perf_event +fn on_event(ctx: *bpf_perf_event_data) -> i32 { + return 0 +} + +fn main() -> i32 { + var prog = load(on_event) + var cache = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: cache_misses, + }, 0) + var branch = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: branch_misses, + group: cache, + }, 0) + var cycles = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: cpu_cycles, + group: cache, + }, 0) + var inst = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: instructions, + group: cache, + }, 0) + var refs = attach(prog, perf_options { + perf_type: perf_type_hardware, + perf_config: cache_references, + group: cache, + }, 0) + detach(refs) + detach(inst) + detach(cycles) + detach(branch) + detach(cache) + detach(prog) + return 0 +} +|} in + try + let _ = make_generated_code_from_source source in + fail "Oversized static perf event group should be rejected at compile time" + with + | Type_error (msg, _) -> + check bool "oversized group reports PMU group limit" true + (contains_substr msg "perf event group rooted at 'cache' needs 5 PMU counter slot(s), but target PMU group limit is 4") + | exn -> + fail ("Expected Type_error for oversized perf event group, got " ^ Printexc.to_string exn) + (* ── Type-checking regression tests ───────────────────────────────────── *) let parse_and_check source = @@ -461,10 +731,17 @@ let tests = [ test_case "perf_event_counting_starts_correctly" `Quick test_perf_event_counting_starts_correctly; test_case "perf_event_period_and_wakeup_defaults" `Quick test_perf_event_period_and_wakeup_defaults; test_case "perf_event_period_and_wakeup_custom" `Quick test_perf_event_period_and_wakeup_custom; + test_case "perf_event_group_fd_codegen" `Quick test_perf_event_group_fd_codegen; + test_case "perf_event_group_member_lifecycle_codegen" `Quick test_perf_event_group_member_lifecycle_codegen; test_case "perf_read_helpers_not_generated" `Quick test_perf_read_helpers_not_generated; test_case "read_helpers_generated_when_used" `Quick test_read_helpers_generated_when_used; + test_case "perf_read_helper_scales_multiplexed_counts"`Quick test_perf_read_helper_scales_multiplexed_counts; test_case "perf_attach_event_function_generated" `Quick test_perf_attach_event_function_generated; test_case "detach_attach_concurrent_window" `Quick test_detach_attach_concurrent_window; + test_case "perf_group_source_field_access_codegen" `Quick test_perf_group_source_field_access_codegen; + test_case "perf_group_attachment_field_codegen" `Quick test_perf_group_attachment_field_codegen; + test_case "perf_read_raw_details_and_group_codegen" `Quick test_perf_read_raw_details_and_group_codegen; + test_case "perf_group_too_large_static_group_rejected" `Quick test_perf_group_too_large_static_group_rejected; test_case "standard_attach_uses_libbpf_error_checks" `Quick test_standard_attach_uses_libbpf_error_checks; ] diff --git a/tests/test_userspace_for_codegen.ml b/tests/test_userspace_for_codegen.ml index 6bddce1..ccf9e7c 100644 --- a/tests/test_userspace_for_codegen.ml +++ b/tests/test_userspace_for_codegen.ml @@ -357,6 +357,37 @@ fn main() -> i32 { with | exn -> fail ("Test failed with exception: " ^ Printexc.to_string exn) +(** Test 9: A later variable declaration with the same name as a for counter + should own the declaration instead of producing a duplicate function-scope + predeclaration. *) +let test_for_counter_reused_by_later_var_decl () = + let program_text = {| +@xdp fn test(ctx: *xdp_md) -> xdp_action { + return 2 +} + +fn main() -> i32 { + var total = 0 + for (i in 0..3) { + total = total + i + } + var i = 0 + while (i < 2) { + i = i + 1 + } + return 0 +} +|} in + + try + let result = generate_userspace_code_from_program program_text "test_for_reuse" in + check bool "for loop still generated" true (contains_pattern result "for.*var_i"); + check bool "later declaration generated once" true (contains_pattern result "uint32_t var_i = 0"); + check bool "no duplicate predeclaration for reused counter" false + (contains_pattern result "uint32_t var_i;[\\s\\S]*uint32_t var_i = 0") + with + | exn -> fail ("Test failed with exception: " ^ Printexc.to_string exn) + (** All global function for statement codegen tests *) let global_function_for_codegen_tests = [ "basic_for_loop_constant_bounds", `Quick, test_basic_for_loop_constant_bounds; @@ -367,6 +398,7 @@ let global_function_for_codegen_tests = [ "for_loop_zero_iterations", `Quick, test_for_loop_zero_iterations; "for_loop_in_helper_function", `Quick, test_for_loop_in_helper_function; "global_functions_vs_ebpf_differences", `Quick, test_global_functions_vs_ebpf_for_loop_differences; + "for_counter_reused_by_later_var_decl", `Quick, test_for_counter_reused_by_later_var_decl; ] let () =