diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-20 07:23:45 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-20 07:23:45 +0300 |
| commit | df1225efe494cc81513cf98e93891376e45f9615 (patch) | |
| tree | 8fe131ba9ae5737022f26fcd60e662c1660329c6 /internal | |
| parent | 11a8642b7035ff558fb84d7761e93525c84e4908 (diff) | |
task 07: add KindMem and separate address-space byte accounting
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/c/generated_tracepoints.c | 20 | ||||
| -rw-r--r-- | internal/c/generated_tracepoints_result.txt | 4 | ||||
| -rw-r--r-- | internal/c/types.h | 14 | ||||
| -rw-r--r-- | internal/event/interface_assertions.go | 3 | ||||
| -rw-r--r-- | internal/event/pair.go | 3 | ||||
| -rw-r--r-- | internal/eventloop_bytes_test.go | 58 | ||||
| -rw-r--r-- | internal/eventloop_exit.go | 43 | ||||
| -rw-r--r-- | internal/eventloop_memory_test.go | 46 | ||||
| -rw-r--r-- | internal/eventloop_runtime.go | 12 | ||||
| -rw-r--r-- | internal/generate/bpfhandler.go | 13 | ||||
| -rw-r--r-- | internal/generate/classify.go | 5 | ||||
| -rw-r--r-- | internal/generate/classify_test.go | 18 | ||||
| -rw-r--r-- | internal/generate/codegen_test.go | 16 | ||||
| -rw-r--r-- | internal/generate/kindregistry.go | 1 | ||||
| -rw-r--r-- | internal/generate/testdata.go | 61 | ||||
| -rw-r--r-- | internal/statsengine/engine.go | 71 | ||||
| -rw-r--r-- | internal/statsengine/engine_reset_test.go | 4 | ||||
| -rw-r--r-- | internal/statsengine/engine_test.go | 57 | ||||
| -rw-r--r-- | internal/statsengine/snapshot.go | 18 | ||||
| -rw-r--r-- | internal/types/fastdecode.go | 21 | ||||
| -rw-r--r-- | internal/types/fastdecode_test.go | 23 | ||||
| -rw-r--r-- | internal/types/generated_types.go | 73 |
22 files changed, 518 insertions, 66 deletions
diff --git a/internal/c/generated_tracepoints.c b/internal/c/generated_tracepoints.c index 393954c..ad11b06 100644 --- a/internal/c/generated_tracepoints.c +++ b/internal/c/generated_tracepoints.c @@ -10736,22 +10736,26 @@ int handle_sys_exit_msync(struct syscall_trace_exit *ctx) { return 0; } -/// sys_enter_mremap is a struct null_event +/// sys_enter_mremap is a struct mem_event SEC("tracepoint/syscalls/sys_enter_mremap") int handle_sys_enter_mremap(struct syscall_trace_enter *ctx) { __u32 pid, tid; if (filter(&pid, &tid)) return 0; - struct null_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct null_event), 0); + struct mem_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct mem_event), 0); if (!ev) return 0; - ev->event_type = ENTER_NULL_EVENT; + ev->event_type = ENTER_MEM_EVENT; ev->trace_id = SYS_ENTER_MREMAP; ev->pid = pid; ev->tid = tid; ev->time = bpf_ktime_get_boot_ns(); + ev->addr = (__u64)ctx->args[0]; + ev->length = (__u64)ctx->args[1]; + ev->length2 = (__u64)ctx->args[2]; + ev->flags = (__u64)ctx->args[3]; bpf_ringbuf_submit(ev, 0); return 0; @@ -11000,22 +11004,26 @@ int handle_sys_exit_brk(struct syscall_trace_exit *ctx) { return 0; } -/// sys_enter_munmap is a struct null_event +/// sys_enter_munmap is a struct mem_event SEC("tracepoint/syscalls/sys_enter_munmap") int handle_sys_enter_munmap(struct syscall_trace_enter *ctx) { __u32 pid, tid; if (filter(&pid, &tid)) return 0; - struct null_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct null_event), 0); + struct mem_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct mem_event), 0); if (!ev) return 0; - ev->event_type = ENTER_NULL_EVENT; + ev->event_type = ENTER_MEM_EVENT; ev->trace_id = SYS_ENTER_MUNMAP; ev->pid = pid; ev->tid = tid; ev->time = bpf_ktime_get_boot_ns(); + ev->addr = (__u64)ctx->args[0]; + ev->length = (__u64)ctx->args[1]; + ev->length2 = 0; + ev->flags = 0; bpf_ringbuf_submit(ev, 0); return 0; diff --git a/internal/c/generated_tracepoints_result.txt b/internal/c/generated_tracepoints_result.txt index 4ba2dc8..77339f5 100644 --- a/internal/c/generated_tracepoints_result.txt +++ b/internal/c/generated_tracepoints_result.txt @@ -181,7 +181,7 @@ sys_enter_mq_open is a struct null_event sys_enter_mq_timedreceive is a struct null_event sys_enter_mq_timedsend is a struct null_event sys_enter_mq_unlink is a struct null_event -sys_enter_mremap is a struct null_event +sys_enter_mremap is a struct mem_event sys_enter_mseal is a struct null_event sys_enter_msgctl is a struct null_event sys_enter_msgget is a struct null_event @@ -190,7 +190,7 @@ sys_enter_msgsnd is a struct null_event sys_enter_msync is a struct null_event sys_enter_munlock is a struct null_event sys_enter_munlockall is a struct null_event -sys_enter_munmap is a struct null_event +sys_enter_munmap is a struct mem_event sys_enter_name_to_handle_at is a struct path_event sys_enter_nanosleep is a struct null_event sys_enter_newfstat is a struct fd_event diff --git a/internal/c/types.h b/internal/c/types.h index 128d4e9..a496c1a 100644 --- a/internal/c/types.h +++ b/internal/c/types.h @@ -35,6 +35,8 @@ #define EXIT_EPOLL_CTL_EVENT 30 #define ENTER_POLL_EVENT 31 #define EXIT_POLL_EVENT 32 +#define ENTER_MEM_EVENT 33 +#define EXIT_MEM_EVENT 34 #define UNCLASSIFIED 0 #define READ_CLASSIFIED 1 @@ -207,3 +209,15 @@ struct poll_event { __s32 nfds; __s64 timeout_ns; }; + +struct mem_event { + __u32 event_type; + __u32 trace_id; + __u64 time; + __u32 pid; + __u32 tid; + __u64 addr; + __u64 length; + __u64 length2; + __u64 flags; +}; diff --git a/internal/event/interface_assertions.go b/internal/event/interface_assertions.go index 8a1c40d..97a198f 100644 --- a/internal/event/interface_assertions.go +++ b/internal/event/interface_assertions.go @@ -62,4 +62,7 @@ var ( // *types.PollEvent carries poll/select argument metadata (nfds and timeout). _ Event = (*types.PollEvent)(nil) + + // *types.MemEvent carries memory-operation metadata (addr/length/flags). + _ Event = (*types.MemEvent)(nil) ) diff --git a/internal/event/pair.go b/internal/event/pair.go index 1626e88..115323b 100644 --- a/internal/event/pair.go +++ b/internal/event/pair.go @@ -26,6 +26,9 @@ type Pair struct { Duration uint64 DurationToPrev uint64 Bytes uint64 // Number of bytes transferred (read/write/transfer syscalls only) + // AddressSpaceBytes tracks memory-region extent for memory syscalls + // (e.g. munmap/mremap) and is intentionally separate from I/O bytes. + AddressSpaceBytes uint64 } func NewPair(enterEv Event) *Pair { diff --git a/internal/eventloop_bytes_test.go b/internal/eventloop_bytes_test.go index a7c25ef..636f80f 100644 --- a/internal/eventloop_bytes_test.go +++ b/internal/eventloop_bytes_test.go @@ -89,3 +89,61 @@ func TestApplyRetBytesForNullEnterRetExitPair(t *testing.T) { t.Fatalf("pair.Bytes = %d, want 4096", pair.Bytes) } } + +func TestAddressSpaceBytesFromMem(t *testing.T) { + tests := []struct { + name string + ev *types.MemEvent + want uint64 + }{ + { + name: "munmap", + ev: &types.MemEvent{TraceId: types.SYS_ENTER_MUNMAP, Length: 4096}, + want: 4096, + }, + { + name: "mremap uses larger extent", + ev: &types.MemEvent{TraceId: types.SYS_ENTER_MREMAP, Length: 4096, Length2: 8192}, + want: 8192, + }, + { + name: "non-memory", + ev: &types.MemEvent{TraceId: types.SYS_ENTER_READ, Length: 123}, + want: 0, + }, + { + name: "nil", + ev: nil, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := addressSpaceBytesFromMem(tt.ev); got != tt.want { + t.Fatalf("addressSpaceBytesFromMem() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestApplyAddressSpaceBytes(t *testing.T) { + pair := &event.Pair{ + EnterEv: &types.MemEvent{ + TraceId: types.SYS_ENTER_MUNMAP, + Length: 16384, + }, + ExitEv: &types.RetEvent{ + TraceId: types.SYS_EXIT_MUNMAP, + Ret: 0, + }, + } + + applyAddressSpaceBytes(pair) + if pair.AddressSpaceBytes != 16384 { + t.Fatalf("pair.AddressSpaceBytes = %d, want 16384", pair.AddressSpaceBytes) + } + if pair.Bytes != 0 { + t.Fatalf("pair.Bytes = %d, want 0 (IO bytes must stay separate)", pair.Bytes) + } +} diff --git a/internal/eventloop_exit.go b/internal/eventloop_exit.go index 239724b..7e26cb0 100644 --- a/internal/eventloop_exit.go +++ b/internal/eventloop_exit.go @@ -40,6 +40,8 @@ func (e *eventLoop) handleTracepointExit(ep *event.Pair) bool { return e.handleEpollCtlExit(ep, ev) case *types.PollEvent: return e.handlePollExit(ep, ev) + case *types.MemEvent: + return e.handleMemExit(ep, ev) case *types.NullEvent: return e.handleNullExit(ep, ev) case *types.FcntlEvent: @@ -406,6 +408,15 @@ func (e *eventLoop) handlePollExit(ep *event.Pair, pollEv *types.PollEvent) bool return true } +func (e *eventLoop) handleMemExit(ep *event.Pair, memEv *types.MemEvent) bool { + ep.Comm = e.comm(memEv.GetTid()) + if !e.Filter().MatchPair(ep) { + ep.Recycle() + return false + } + return true +} + func pipeDescriptorName(flags, fd0, fd1 int32) string { return fmt.Sprintf("pipe:%d:%d:%d", flags, fd0, fd1) } @@ -518,6 +529,21 @@ func applyRetBytes(ep *event.Pair) { ep.Bytes = bytesFromRet(retEv) } +func applyAddressSpaceBytes(ep *event.Pair) { + if ep == nil { + return + } + memEv, ok := ep.EnterEv.(*types.MemEvent) + if !ok { + return + } + retEv, ok := ep.ExitEv.(*types.RetEvent) + if !ok || retEv.Ret < 0 { + return + } + ep.AddressSpaceBytes = addressSpaceBytesFromMem(memEv) +} + // dropMalformedRawEvent records a warning when a raw BPF event cannot be // decoded, keeping the error visible without crashing the event loop. func (e *eventLoop) dropMalformedRawEvent(evType types.EventType, raw []byte) { @@ -537,3 +563,20 @@ func bytesFromRet(retEv *types.RetEvent) uint64 { return 0 } } + +func addressSpaceBytesFromMem(memEv *types.MemEvent) uint64 { + if memEv == nil { + return 0 + } + switch memEv.GetTraceId() { + case types.SYS_ENTER_MUNMAP: + return memEv.Length + case types.SYS_ENTER_MREMAP: + if memEv.Length > memEv.Length2 { + return memEv.Length + } + return memEv.Length2 + default: + return 0 + } +} diff --git a/internal/eventloop_memory_test.go b/internal/eventloop_memory_test.go new file mode 100644 index 0000000..4ac9580 --- /dev/null +++ b/internal/eventloop_memory_test.go @@ -0,0 +1,46 @@ +package internal + +import ( + "testing" + + "ior/internal/event" + "ior/internal/globalfilter" + "ior/internal/types" +) + +func TestHandleMemExitAppliesPairFilter(t *testing.T) { + el := mustNewEventLoop(t, eventLoopConfig{ + filter: globalfilter.Filter{ + Syscall: &globalfilter.StringFilter{Pattern: "openat"}, + }, + }) + + enter := &types.MemEvent{ + EventType: types.ENTER_MEM_EVENT, + TraceId: types.SYS_ENTER_MUNMAP, + Time: 100, + Pid: 91, + Tid: 92, + Length: 4096, + } + exit := &types.RetEvent{ + EventType: types.EXIT_RET_EVENT, + TraceId: types.SYS_EXIT_MUNMAP, + Time: 200, + Pid: 91, + Tid: 92, + Ret: 0, + } + ep := &event.Pair{EnterEv: enter, ExitEv: exit} + + if ok := el.handleMemExit(ep, enter); ok { + t.Fatal("handleMemExit should reject pair due to filter mismatch") + } +} + +func TestInitRawHandlersRegistersMemoryEvents(t *testing.T) { + el := mustNewEventLoop(t, eventLoopConfig{}) + if _, ok := el.rawHandlers[types.ENTER_MEM_EVENT]; !ok { + t.Fatal("ENTER_MEM_EVENT handler is not registered") + } +} diff --git a/internal/eventloop_runtime.go b/internal/eventloop_runtime.go index 3c1217b..35714a1 100644 --- a/internal/eventloop_runtime.go +++ b/internal/eventloop_runtime.go @@ -149,6 +149,7 @@ func (e *eventLoop) initRawHandlers() { e.registerSocketHandlers() e.registerIPCHandlers() e.registerPollingHandlers() + e.registerMemoryHandlers() } // registerOpenHandlers wires enter/exit handlers for open-family events. @@ -351,6 +352,16 @@ func (e *eventLoop) registerPollingHandlers() { } } +func (e *eventLoop) registerMemoryHandlers() { + e.rawHandlers[types.ENTER_MEM_EVENT] = func(raw []byte, _ chan<- *event.Pair) { + memEv, ok := decodeRawEvent(e, types.ENTER_MEM_EVENT, raw, types.NewMemEventFast) + if !ok { + return + } + e.tracepointEntered(memEv) + } +} + func decodeRawEvent[T any](e *eventLoop, eventType types.EventType, raw []byte, decode func([]byte) *T) (*T, bool) { decoded := decode(raw) if decoded == nil { @@ -404,6 +415,7 @@ func (e *eventLoop) tracepointExited(exitEv event.Event, ch chan<- *event.Pair) return } applyRetBytes(ep) + applyAddressSpaceBytes(ep) tid := ep.EnterEv.GetTid() ep.CalculateDurations(e.pairs.prevTime(tid)) e.pairs.setPrevTime(tid, ep.ExitEv.GetTime()) diff --git a/internal/generate/bpfhandler.go b/internal/generate/bpfhandler.go index 20859ee..e3d0d67 100644 --- a/internal/generate/bpfhandler.go +++ b/internal/generate/bpfhandler.go @@ -87,6 +87,8 @@ func generateExtra(tp GeneratedTracepoint, isEnter bool) string { return generateExtraEpollCtl() case KindPoll: return generateExtraPoll(f.Name) + case KindMem: + return generateExtraMem(f.Name) case KindOpen: return generateExtraOpen(f) case KindPathname: @@ -222,6 +224,17 @@ func generateExtraPoll(name string) string { } } +func generateExtraMem(name string) string { + switch name { + case "sys_enter_munmap": + return " ev->addr = (__u64)ctx->args[0];\n ev->length = (__u64)ctx->args[1];\n ev->length2 = 0;\n ev->flags = 0;\n" + case "sys_enter_mremap": + return " ev->addr = (__u64)ctx->args[0];\n ev->length = (__u64)ctx->args[1];\n ev->length2 = (__u64)ctx->args[2];\n ev->flags = (__u64)ctx->args[3];\n" + default: + return " ev->addr = 0;\n ev->length = 0;\n ev->length2 = 0;\n ev->flags = 0;\n" + } +} + // eventStructName returns the C struct name for a TracepointKind. The mapping // is driven by kindRegistry so adding a new kind only requires a registry entry. func eventStructName(kind TracepointKind) string { diff --git a/internal/generate/classify.go b/internal/generate/classify.go index 7c391fb..a2da47d 100644 --- a/internal/generate/classify.go +++ b/internal/generate/classify.go @@ -22,6 +22,7 @@ const ( KindEventfd KindEpollCtl KindPoll + KindMem ) type RetClassification string @@ -148,6 +149,10 @@ func classifyNameOnly(name string) (ClassificationResult, bool) { return ClassificationResult{Kind: KindPoll}, true case "sys_enter_pselect6": return ClassificationResult{Kind: KindPoll}, true + case "sys_enter_munmap": + return ClassificationResult{Kind: KindMem}, true + case "sys_enter_mremap": + return ClassificationResult{Kind: KindMem}, true } if strings.HasPrefix(name, "sys_enter_io_") { return ClassificationResult{Kind: KindNull}, true diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go index 367d7c8..2bc0e88 100644 --- a/internal/generate/classify_test.go +++ b/internal/generate/classify_test.go @@ -425,6 +425,20 @@ func TestClassifyPselect6(t *testing.T) { } } +func TestClassifyMunmap(t *testing.T) { + r := classifyFromData(t, FormatMunmap) + if r.Kind != KindMem { + t.Errorf("munmap: got kind %d, want KindMem", r.Kind) + } +} + +func TestClassifyMremap(t *testing.T) { + r := classifyFromData(t, FormatMremap) + if r.Kind != KindMem { + t.Errorf("mremap: got kind %d, want KindMem", r.Kind) + } +} + func TestClassifyKillRequiresGenerationFallback(t *testing.T) { r := classifyFromData(t, FormatKill) if r.Kind != KindNone { @@ -477,6 +491,8 @@ func TestClassifySyscallPairAccepted(t *testing.T) { {"ppoll", FormatPpoll, FormatExitPpoll, KindPoll}, {"select", FormatSelect, FormatExitSelect, KindPoll}, {"pselect6", FormatPselect6, FormatExitPselect6, KindPoll}, + {"munmap", FormatMunmap, FormatExitMunmap, KindMem}, + {"mremap", FormatMremap, FormatExitMremap, KindMem}, {"kill", FormatKill, FormatExitKill, KindNull}, } @@ -514,6 +530,8 @@ func TestClassifySyscallPairEmitsAllFamilies(t *testing.T) { {"ppoll", FormatPpoll, FormatExitPpoll, FamilyPolling}, {"select", FormatSelect, FormatExitSelect, FamilyPolling}, {"pselect6", FormatPselect6, FormatExitPselect6, FamilyPolling}, + {"munmap", FormatMunmap, FormatExitMunmap, FamilyMemory}, + {"mremap", FormatMremap, FormatExitMremap, FamilyMemory}, {"kill", FormatKill, FormatExitKill, FamilySignals}, } diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go index a3baed7..3ec8d72 100644 --- a/internal/generate/codegen_test.go +++ b/internal/generate/codegen_test.go @@ -168,6 +168,18 @@ func TestGenerateMmapHandlerUsesFdArgumentIndex(t *testing.T) { requireContains(t, output, "ev->fd = (__s32)ctx->args[4];") } +func TestGenerateMemHandler(t *testing.T) { + output := generateFromPair(t, FormatMremap, FormatExitMremap) + + requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_mremap")`) + requireContains(t, output, "struct mem_event *ev") + requireContains(t, output, "ev->event_type = ENTER_MEM_EVENT;") + requireContains(t, output, "ev->addr = (__u64)ctx->args[0];") + requireContains(t, output, "ev->length = (__u64)ctx->args[1];") + requireContains(t, output, "ev->length2 = (__u64)ctx->args[2];") + requireContains(t, output, "ev->flags = (__u64)ctx->args[3];") +} + func TestGenerateDup3Handler(t *testing.T) { output := generateFromPair(t, FormatDup3, FormatExitDup3) @@ -412,6 +424,7 @@ func TestGenerateAllEventTypes(t *testing.T) { {KindEventfd, "ENTER_EVENTFD_EVENT", "EXIT_EVENTFD_EVENT"}, {KindEpollCtl, "ENTER_EPOLL_CTL_EVENT", "EXIT_EPOLL_CTL_EVENT"}, {KindPoll, "ENTER_POLL_EVENT", "EXIT_POLL_EVENT"}, + {KindMem, "ENTER_MEM_EVENT", "EXIT_MEM_EVENT"}, } for _, tt := range tests { @@ -445,6 +458,7 @@ func TestEventStructNames(t *testing.T) { {KindEventfd, "eventfd_event"}, {KindEpollCtl, "epoll_ctl_event"}, {KindPoll, "poll_event"}, + {KindMem, "mem_event"}, } for _, tt := range tests { @@ -463,7 +477,7 @@ func TestEnterReject(t *testing.T) { t.Error("KindNone should be enter-rejected") } - accepted := []TracepointKind{KindFd, KindOpen, KindPathname, KindName, KindFcntl, KindNull, KindDup3, KindOpenByHandleAt, KindSocket, KindSocketpair, KindAccept, KindPipe, KindEventfd, KindEpollCtl, KindPoll} + accepted := []TracepointKind{KindFd, KindOpen, KindPathname, KindName, KindFcntl, KindNull, KindDup3, KindOpenByHandleAt, KindSocket, KindSocketpair, KindAccept, KindPipe, KindEventfd, KindEpollCtl, KindPoll, KindMem} for _, k := range accepted { if isEnterRejected(k) { t.Errorf("kind %d should NOT be enter-rejected", k) diff --git a/internal/generate/kindregistry.go b/internal/generate/kindregistry.go index e8efe99..756ed17 100644 --- a/internal/generate/kindregistry.go +++ b/internal/generate/kindregistry.go @@ -32,6 +32,7 @@ var kindRegistry = map[TracepointKind]kindMeta{ KindEventfd: {structName: "eventfd_event", enterAccepted: true}, KindEpollCtl: {structName: "epoll_ctl_event", enterAccepted: true}, KindPoll: {structName: "poll_event", enterAccepted: true}, + KindMem: {structName: "mem_event", enterAccepted: true}, // KindNone is intentionally absent: it represents "unclassified" and is // never enter-accepted. lookupKind returns the zero kindMeta (enterAccepted=false) // for any unregistered kind, so KindNone is implicitly rejected. diff --git a/internal/generate/testdata.go b/internal/generate/testdata.go index e3e3036..6b08f98 100644 --- a/internal/generate/testdata.go +++ b/internal/generate/testdata.go @@ -201,6 +201,67 @@ format: print fmt: "0x%lx", REC->ret ` +const FormatMunmap = `name: sys_enter_munmap +ID: 696 +format: + field:unsigned short common_type; offset:0; size:2; signed:0; + field:unsigned char common_flags; offset:2; size:1; signed:0; + field:unsigned char common_preempt_count; offset:3; size:1; signed:0; + field:int common_pid; offset:4; size:4; signed:1; + + field:int __syscall_nr; offset:8; size:4; signed:1; + field:unsigned long addr; offset:16; size:8; signed:0; + field:size_t len; offset:24; size:8; signed:0; + +print fmt: "addr: 0x%08lx, len: 0x%08lx", ((unsigned long)(REC->addr)), ((unsigned long)(REC->len)) +` + +const FormatExitMunmap = `name: sys_exit_munmap +ID: 695 +format: + field:unsigned short common_type; offset:0; size:2; signed:0; + field:unsigned char common_flags; offset:2; size:1; signed:0; + field:unsigned char common_preempt_count; offset:3; size:1; signed:0; + field:int common_pid; offset:4; size:4; signed:1; + + field:int __syscall_nr; offset:8; size:4; signed:1; + field:long ret; offset:16; size:8; signed:1; + +print fmt: "0x%lx", REC->ret +` + +const FormatMremap = `name: sys_enter_mremap +ID: 710 +format: + field:unsigned short common_type; offset:0; size:2; signed:0; + field:unsigned char common_flags; offset:2; size:1; signed:0; + field:unsigned char common_preempt_count; offset:3; size:1; signed:0; + field:int common_pid; offset:4; size:4; signed:1; + + field:int __syscall_nr; offset:8; size:4; signed:1; + field:unsigned long addr; offset:16; size:8; signed:0; + field:unsigned long old_len; offset:24; size:8; signed:0; + field:unsigned long new_len; offset:32; size:8; signed:0; + field:unsigned long flags; offset:40; size:8; signed:0; + field:unsigned long new_addr; offset:48; size:8; signed:0; + +print fmt: "addr: 0x%08lx, old_len: 0x%08lx, new_len: 0x%08lx, flags: 0x%08lx, new_addr: 0x%08lx", ((unsigned long)(REC->addr)), ((unsigned long)(REC->old_len)), ((unsigned long)(REC->new_len)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->new_addr)) +` + +const FormatExitMremap = `name: sys_exit_mremap +ID: 709 +format: + field:unsigned short common_type; offset:0; size:2; signed:0; + field:unsigned char common_flags; offset:2; size:1; signed:0; + field:unsigned char common_preempt_count; offset:3; size:1; signed:0; + field:int common_pid; offset:4; size:4; signed:1; + + field:int __syscall_nr; offset:8; size:4; signed:1; + field:long ret; offset:16; size:8; signed:1; + +print fmt: "0x%lx", REC->ret +` + const FormatExitMsync = `name: sys_exit_msync ID: 1028 format: diff --git a/internal/statsengine/engine.go b/internal/statsengine/engine.go index 7d85e96..85017fd 100644 --- a/internal/statsengine/engine.go +++ b/internal/statsengine/engine.go @@ -47,13 +47,14 @@ type Engine struct { startedAt time.Time topN int - totalSyscalls uint64 - totalErrors uint64 - totalBytes uint64 - totalReadBytes uint64 - totalWriteBytes uint64 - totalLatency uint64 - totalGap uint64 + totalSyscalls uint64 + totalErrors uint64 + totalBytes uint64 + totalAddressSpaceBytes uint64 + totalReadBytes uint64 + totalWriteBytes uint64 + totalLatency uint64 + totalGap uint64 syscalls *syscallAccumulator families *familyAccumulator @@ -70,13 +71,14 @@ type snapshotInputs struct { now time.Time startedAt time.Time - totalSyscalls uint64 - totalErrors uint64 - totalBytes uint64 - totalReadBytes uint64 - totalWriteBytes uint64 - totalLatency uint64 - totalGap uint64 + totalSyscalls uint64 + totalErrors uint64 + totalBytes uint64 + totalAddressSpaceBytes uint64 + totalReadBytes uint64 + totalWriteBytes uint64 + totalLatency uint64 + totalGap uint64 latencySeries []float64 gapSeries []float64 @@ -130,6 +132,7 @@ func (e *Engine) Reset() { e.totalSyscalls = 0 e.totalErrors = 0 e.totalBytes = 0 + e.totalAddressSpaceBytes = 0 e.totalReadBytes = 0 e.totalWriteBytes = 0 e.totalLatency = 0 @@ -157,6 +160,7 @@ func (e *Engine) Ingest(pair *event.Pair) { now := e.now() e.totalSyscalls++ e.totalBytes += pair.Bytes + e.totalAddressSpaceBytes += pair.AddressSpaceBytes e.totalLatency += pair.Duration e.totalGap += pair.DurationToPrev @@ -209,24 +213,25 @@ func (e *Engine) captureSnapshotInputs() snapshotInputs { defer e.mu.Unlock() return snapshotInputs{ - now: e.now(), - startedAt: e.startedAt, - totalSyscalls: e.totalSyscalls, - totalErrors: e.totalErrors, - totalBytes: e.totalBytes, - totalReadBytes: e.totalReadBytes, - totalWriteBytes: e.totalWriteBytes, - totalLatency: e.totalLatency, - totalGap: e.totalGap, - latencySeries: e.latencySeries.Values(), - gapSeries: e.gapSeries.Values(), - throughputSeries: e.throughputSeries.Values(), - syscalls: e.syscalls.snapshotInputs(), - families: e.families.snapshotInputs(), - files: e.files.snapshotInputs(), - processes: e.processes.snapshotInputs(), - latencyHist: e.latencyHist.snapshotInputs(), - gapHist: e.gapHist.snapshotInputs(), + now: e.now(), + startedAt: e.startedAt, + totalSyscalls: e.totalSyscalls, + totalErrors: e.totalErrors, + totalBytes: e.totalBytes, + totalAddressSpaceBytes: e.totalAddressSpaceBytes, + totalReadBytes: e.totalReadBytes, + totalWriteBytes: e.totalWriteBytes, + totalLatency: e.totalLatency, + totalGap: e.totalGap, + latencySeries: e.latencySeries.Values(), + gapSeries: e.gapSeries.Values(), + throughputSeries: e.throughputSeries.Values(), + syscalls: e.syscalls.snapshotInputs(), + families: e.families.snapshotInputs(), + files: e.files.snapshotInputs(), + processes: e.processes.snapshotInputs(), + latencyHist: e.latencyHist.snapshotInputs(), + gapHist: e.gapHist.snapshotInputs(), } } @@ -287,8 +292,10 @@ func populateSnapshotFields(snap *Snapshot, in snapshotInputs, elapsed time.Dura snap.TotalSyscalls = in.totalSyscalls snap.TotalErrors = in.totalErrors snap.TotalBytes = in.totalBytes + snap.TotalAddressSpaceBytes = in.totalAddressSpaceBytes snap.SyscallRatePerSec = safeRate(in.totalSyscalls, rateDiv) snap.ErrorRatePerSec = safeRate(in.totalErrors, rateDiv) + snap.AddressSpaceBytesPerSec = safeRate(in.totalAddressSpaceBytes, rateDiv) snap.ReadBytesPerSec = safeRate(in.totalReadBytes, rateDiv) snap.WriteBytesPerSec = safeRate(in.totalWriteBytes, rateDiv) snap.LatencyMeanNs = safeMean(in.totalLatency, in.totalSyscalls) diff --git a/internal/statsengine/engine_reset_test.go b/internal/statsengine/engine_reset_test.go index c09c059..5e8388a 100644 --- a/internal/statsengine/engine_reset_test.go +++ b/internal/statsengine/engine_reset_test.go @@ -9,7 +9,7 @@ import ( func TestEngineResetClearsAccumulatedStats(t *testing.T) { e := NewEngine(8) - e.Ingest(newEnginePair(types.SYS_ENTER_READ, 7, types.READ_CLASSIFIED, "test", 1, "/tmp/a", 7, 1000, 50)) + e.Ingest(newEnginePair(types.SYS_ENTER_READ, 7, types.READ_CLASSIFIED, "test", 1, "/tmp/a", 7, 512, 1000, 50)) before, err := e.Snapshot() if err != nil { t.Fatalf("unexpected snapshot error: %v", err) @@ -23,7 +23,7 @@ func TestEngineResetClearsAccumulatedStats(t *testing.T) { if err != nil { t.Fatalf("unexpected snapshot error after reset: %v", err) } - if after.TotalSyscalls != 0 || after.TotalBytes != 0 || after.TotalErrors != 0 { + if after.TotalSyscalls != 0 || after.TotalBytes != 0 || after.TotalAddressSpaceBytes != 0 || after.TotalErrors != 0 { t.Fatalf("expected totals cleared after reset, got %+v", after) } if after.Elapsed > 2*time.Second { diff --git a/internal/statsengine/engine_test.go b/internal/statsengine/engine_test.go index 9543405..0500d20 100644 --- a/internal/statsengine/engine_test.go +++ b/internal/statsengine/engine_test.go @@ -26,11 +26,11 @@ func TestEngineIngestAndSnapshotIntegration(t *testing.T) { clock := &fakeClock{now: time.Unix(1000, 0)} engine := newEngineWithClock(2, clock.Now) - engine.Ingest(newEnginePair(types.SYS_ENTER_READ, 100, types.READ_CLASSIFIED, "proc-a", 1, "/tmp/a", 100, 10, 3)) + engine.Ingest(newEnginePair(types.SYS_ENTER_READ, 100, types.READ_CLASSIFIED, "proc-a", 1, "/tmp/a", 100, 0, 10, 3)) clock.Advance(500 * time.Millisecond) - engine.Ingest(newEnginePair(types.SYS_ENTER_WRITE, -1, types.WRITE_CLASSIFIED, "proc-a", 1, "/tmp/a", 50, 20, 5)) + engine.Ingest(newEnginePair(types.SYS_ENTER_WRITE, -1, types.WRITE_CLASSIFIED, "proc-a", 1, "/tmp/a", 50, 0, 20, 5)) clock.Advance(500 * time.Millisecond) - engine.Ingest(newEnginePair(types.SYS_ENTER_COPY_FILE_RANGE, 80, types.TRANSFER_CLASSIFIED, "proc-b", 2, "/tmp/b", 20, 40, 8)) + engine.Ingest(newEnginePair(types.SYS_ENTER_COPY_FILE_RANGE, 80, types.TRANSFER_CLASSIFIED, "proc-b", 2, "/tmp/b", 20, 0, 40, 8)) clock.Advance(1 * time.Second) snap, err := engine.Snapshot() @@ -44,6 +44,9 @@ func TestEngineIngestAndSnapshotIntegration(t *testing.T) { if snap.TotalSyscalls != 3 || snap.TotalErrors != 1 || snap.TotalBytes != 170 { t.Fatalf("unexpected totals: syscalls=%d errors=%d bytes=%d", snap.TotalSyscalls, snap.TotalErrors, snap.TotalBytes) } + if snap.TotalAddressSpaceBytes != 0 { + t.Fatalf("unexpected address-space total: %d", snap.TotalAddressSpaceBytes) + } if snap.LatencyMeanNs != (10+20+40)/3.0 { t.Fatalf("unexpected latency mean: %v", snap.LatencyMeanNs) } @@ -89,10 +92,10 @@ func TestEngineAggregatesSyscallFamilies(t *testing.T) { clock := &fakeClock{now: time.Unix(3000, 0)} engine := newEngineWithClock(10, clock.Now) - engine.Ingest(newEnginePair(types.SYS_ENTER_EPOLL_WAIT, 0, types.UNCLASSIFIED, "poller", 1, "", 0, 100, 1)) - engine.Ingest(newEnginePair(types.SYS_ENTER_POLL, -1, types.UNCLASSIFIED, "poller", 1, "", 0, 300, 2)) - engine.Ingest(newEnginePair(types.SYS_ENTER_GETPID, 0, types.UNCLASSIFIED, "proc", 2, "", 0, 50, 3)) - engine.Ingest(newEnginePair(types.SYS_ENTER_READ, 4, types.READ_CLASSIFIED, "reader", 3, "/tmp/a", 4, 25, 4)) + engine.Ingest(newEnginePair(types.SYS_ENTER_EPOLL_WAIT, 0, types.UNCLASSIFIED, "poller", 1, "", 0, 0, 100, 1)) + engine.Ingest(newEnginePair(types.SYS_ENTER_POLL, -1, types.UNCLASSIFIED, "poller", 1, "", 0, 0, 300, 2)) + engine.Ingest(newEnginePair(types.SYS_ENTER_GETPID, 0, types.UNCLASSIFIED, "proc", 2, "", 0, 0, 50, 3)) + engine.Ingest(newEnginePair(types.SYS_ENTER_READ, 4, types.READ_CLASSIFIED, "reader", 3, "/tmp/a", 4, 0, 25, 4)) clock.Advance(time.Second) snap, err := engine.Snapshot() @@ -148,6 +151,29 @@ func TestEngineSnapshotWithNoEvents(t *testing.T) { } } +func TestEngineTracksAddressSpaceBytesSeparately(t *testing.T) { + clock := &fakeClock{now: time.Unix(4000, 0)} + engine := newEngineWithClock(10, clock.Now) + + engine.Ingest(newEnginePair(types.SYS_ENTER_MUNMAP, 0, types.UNCLASSIFIED, "proc", 1, "", 0, 4096, 10, 1)) + engine.Ingest(newEnginePair(types.SYS_ENTER_MREMAP, 0, types.UNCLASSIFIED, "proc", 1, "", 0, 8192, 20, 2)) + clock.Advance(2 * time.Second) + + snap, err := engine.Snapshot() + if err != nil { + t.Fatalf("Snapshot() error = %v", err) + } + if snap.TotalBytes != 0 { + t.Fatalf("TotalBytes = %d, want 0 for non-IO memory operations", snap.TotalBytes) + } + if snap.TotalAddressSpaceBytes != 12288 { + t.Fatalf("TotalAddressSpaceBytes = %d, want 12288", snap.TotalAddressSpaceBytes) + } + if math.Abs(snap.AddressSpaceBytesPerSec-6144.0) > 1e-9 { + t.Fatalf("AddressSpaceBytesPerSec = %v, want 6144", snap.AddressSpaceBytesPerSec) + } +} + func TestEngineTrendDetection(t *testing.T) { if got := detectTrend(make([]float64, trendWindowSlots*2)); got.Direction != TrendStable { t.Fatalf("expected stable for flat data, got %+v", got) @@ -175,14 +201,15 @@ func TestEngineTrendDetection(t *testing.T) { } } -func newEnginePair(traceID types.TraceId, ret int64, retType uint32, comm string, pid uint32, path string, bytes uint64, duration uint64, gap uint64) *event.Pair { +func newEnginePair(traceID types.TraceId, ret int64, retType uint32, comm string, pid uint32, path string, bytes uint64, addressSpaceBytes uint64, duration uint64, gap uint64) *event.Pair { return &event.Pair{ - EnterEv: &types.RetEvent{TraceId: traceID, Pid: pid}, - ExitEv: &types.RetEvent{TraceId: traceID, Pid: pid, Ret: ret, RetType: retType}, - Comm: comm, - Duration: duration, - DurationToPrev: gap, - Bytes: bytes, - File: file.NewFd(3, path, -1), + EnterEv: &types.RetEvent{TraceId: traceID, Pid: pid}, + ExitEv: &types.RetEvent{TraceId: traceID, Pid: pid, Ret: ret, RetType: retType}, + Comm: comm, + Duration: duration, + DurationToPrev: gap, + Bytes: bytes, + AddressSpaceBytes: addressSpaceBytes, + File: file.NewFd(3, path, -1), } } diff --git a/internal/statsengine/snapshot.go b/internal/statsengine/snapshot.go index 859cd2e..bec92fb 100644 --- a/internal/statsengine/snapshot.go +++ b/internal/statsengine/snapshot.go @@ -30,14 +30,16 @@ type Snapshot struct { GeneratedAt time.Time Elapsed time.Duration - TotalSyscalls uint64 - TotalErrors uint64 - TotalBytes uint64 - - SyscallRatePerSec float64 - ErrorRatePerSec float64 - ReadBytesPerSec float64 - WriteBytesPerSec float64 + TotalSyscalls uint64 + TotalErrors uint64 + TotalBytes uint64 + TotalAddressSpaceBytes uint64 + + SyscallRatePerSec float64 + ErrorRatePerSec float64 + AddressSpaceBytesPerSec float64 + ReadBytesPerSec float64 + WriteBytesPerSec float64 LatencyMeanNs float64 GapMeanNs float64 diff --git a/internal/types/fastdecode.go b/internal/types/fastdecode.go index 9c0ac9c..1f89ee4 100644 --- a/internal/types/fastdecode.go +++ b/internal/types/fastdecode.go @@ -24,6 +24,7 @@ const ( epollCtlEventSize = 40 pollEventSize = 40 pollEventSizeV1 = 36 + memEventSize = 56 ) func NewOpenEventFast(raw []byte) *OpenEvent { @@ -339,3 +340,23 @@ func NewPollEventFast(raw []byte) *PollEvent { p.TimeoutNs = int64(binary.LittleEndian.Uint64(raw[timeoutOffset : timeoutOffset+8])) return p } + +func NewMemEventFast(raw []byte) *MemEvent { + if len(raw) < memEventSize { + return nil + } + if len(raw) != memEventSize { + return NewMemEvent(raw) + } + m := poolOfMemEvents.Get().(*MemEvent) + m.EventType = EventType(binary.LittleEndian.Uint32(raw[0:4])) + m.TraceId = TraceId(binary.LittleEndian.Uint32(raw[4:8])) + m.Time = binary.LittleEndian.Uint64(raw[8:16]) + m.Pid = binary.LittleEndian.Uint32(raw[16:20]) + m.Tid = binary.LittleEndian.Uint32(raw[20:24]) + m.Addr = binary.LittleEndian.Uint64(raw[24:32]) + m.Length = binary.LittleEndian.Uint64(raw[32:40]) + m.Length2 = binary.LittleEndian.Uint64(raw[40:48]) + m.Flags = binary.LittleEndian.Uint64(raw[48:56]) + return m +} diff --git a/internal/types/fastdecode_test.go b/internal/types/fastdecode_test.go index 272e6d4..8bb4b34 100644 --- a/internal/types/fastdecode_test.go +++ b/internal/types/fastdecode_test.go @@ -218,6 +218,29 @@ func TestFastDecodersMatchGeneratedDecoders(t *testing.T) { t.Fatalf("poll decode mismatch") } }) + + t.Run("MemEvent", func(t *testing.T) { + ev := &MemEvent{ + EventType: ENTER_MEM_EVENT, + TraceId: SYS_ENTER_MREMAP, + Time: 1, + Pid: 2, + Tid: 3, + Addr: 0x1000, + Length: 4096, + Length2: 8192, + Flags: 1, + } + raw, _ := ev.Bytes() + + slow := NewMemEvent(raw) + fast := NewMemEventFast(raw) + defer slow.Recycle() + defer fast.Recycle() + if !slow.Equals(fast) { + t.Fatalf("mem decode mismatch") + } + }) } func TestNewSocketpairEventFastKernelLayout(t *testing.T) { diff --git a/internal/types/generated_types.go b/internal/types/generated_types.go index 6f95c7a..63cb28c 100644 --- a/internal/types/generated_types.go +++ b/internal/types/generated_types.go @@ -100,6 +100,8 @@ const ENTER_EPOLL_CTL_EVENT = 29 const EXIT_EPOLL_CTL_EVENT = 30 const ENTER_POLL_EVENT = 31 const EXIT_POLL_EVENT = 32 +const ENTER_MEM_EVENT = 33 +const EXIT_MEM_EVENT = 34 const UNCLASSIFIED = 0 const READ_CLASSIFIED = 1 const WRITE_CLASSIFIED = 2 @@ -1948,3 +1950,74 @@ func (p *PollEvent) Bytes() ([]byte, error) { func (p *PollEvent) Recycle() { poolOfPollEvents.Put(p) } + +type MemEvent struct { + EventType EventType + TraceId TraceId + Time uint64 + Pid uint32 + Tid uint32 + Addr uint64 + Length uint64 + Length2 uint64 + Flags uint64 +} + +func (m MemEvent) String() string { + return fmt.Sprintf("EventType:%v TraceId:%v Time:%v Pid:%v Tid:%v Addr:%v Length:%v Length2:%v Flags:%v", m.EventType, m.TraceId, m.Time, m.Pid, m.Tid, m.Addr, m.Length, m.Length2, m.Flags) +} + +func (m MemEvent) Equals(other any) bool { + otherConcrete, ok := other.(*MemEvent) + if !ok { + return false + } + return m.EventType == otherConcrete.EventType && m.TraceId == otherConcrete.TraceId && m.Time == otherConcrete.Time && m.Pid == otherConcrete.Pid && m.Tid == otherConcrete.Tid && m.Addr == otherConcrete.Addr && m.Length == otherConcrete.Length && m.Length2 == otherConcrete.Length2 && m.Flags == otherConcrete.Flags +} + +func (m *MemEvent) GetEventType() EventType { + return m.EventType +} + +func (m *MemEvent) GetTraceId() TraceId { + return m.TraceId +} + +func (m *MemEvent) GetPid() uint32 { + return m.Pid +} + +func (m *MemEvent) GetTid() uint32 { + return m.Tid +} + +func (m *MemEvent) GetTime() uint64 { + return m.Time +} + +var poolOfMemEvents = sync.Pool{ + New: func() any { return &MemEvent{} }, +} + +func NewMemEvent(raw []byte) *MemEvent { + m := poolOfMemEvents.Get().(*MemEvent) + if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, m); err != nil { + *m = MemEvent{} + poolOfMemEvents.Put(m) + return nil + } + return m +} + +func (m *MemEvent) Bytes() ([]byte, error) { + buf := new(bytes.Buffer) + err := binary.Write(buf, binary.LittleEndian, m) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (m *MemEvent) Recycle() { + poolOfMemEvents.Put(m) +} |
