Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4cd9cff

Browse files
committedMay 15, 2024
luajit stack unwinding support
Works with openresty and luajit with caveats. lua_State is looked up via uprobes on lua_pcall and lua_resume. If uprobes aren't enabled an attempt will be made to look up lua_State via nginx_lua_co_ctx object which seems to work well for openresty but isn't well tested. Fixes: parca-dev#1889
1 parent 369ee3a commit 4cd9cff

File tree

24 files changed

+3514
-24
lines changed

24 files changed

+3514
-24
lines changed
 

‎Makefile

+5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ OUT_BPF := $(OUT_BPF_DIR)/native.bpf.o
6464
OUT_RBPERF := $(OUT_BPF_DIR)/rbperf.bpf.o
6565
OUT_PYPERF := $(OUT_BPF_DIR)/pyperf.bpf.o
6666
OUT_JVM := $(OUT_BPF_DIR)/jvm.bpf.o
67+
OUT_LUA := $(OUT_BPF_DIR)/lua.bpf.o
6768
OUT_BPF_CONTAINED_DIR := pkg/contained/bpf/$(ARCH)
6869
OUT_PID_NAMESPACE := $(OUT_BPF_CONTAINED_DIR)/pid_namespace.bpf.o
6970

@@ -167,6 +168,7 @@ $(OUT_BPF): $(BPF_SRC) libbpf | $(OUT_DIR)
167168
cp bpf/out/$(ARCH)/rbperf.bpf.o $(OUT_RBPERF)
168169
cp bpf/out/$(ARCH)/pyperf.bpf.o $(OUT_PYPERF)
169170
cp bpf/out/$(ARCH)/jvm.bpf.o $(OUT_JVM)
171+
cp bpf/out/$(ARCH)/lua.bpf.o $(OUT_LUA)
170172
cp bpf/out/$(ARCH)/pid_namespace.bpf.o $(OUT_PID_NAMESPACE)
171173
else
172174
$(OUT_BPF): $(DOCKER_BUILDER) | $(OUT_DIR)
@@ -240,6 +242,9 @@ test/integration/ruby: $(GO_SRC) $(LIBBPF_HEADERS) $(LIBBPF_OBJ) bpf
240242
test/integration/java: $(GO_SRC) $(LIBBPF_HEADERS) $(LIBBPF_OBJ) bpf
241243
sudo --preserve-env=CI,C_INCLUDE_PATH,LIBRARY_PATH,PKG_CONFIG_PATH $(GO_ENV) $(CGO_ENV) $(GO) test $(SANITIZERS) -v ./test/integration/java -count=1
242244

245+
test/integration/lua: $(GO_SRC) $(LIBBPF_HEADERS) $(LIBBPF_OBJ) bpf
246+
sudo --preserve-env=CI,C_INCLUDE_PATH,LIBRARY_PATH,PKG_CONFIG_PATH $(GO_ENV) $(CGO_ENV) $(GO) test $(SANITIZERS) -v ./test/integration/lua -count=1
247+
243248
.PHONY: integration-stress
244249
integration-stress:
245250
sudo --preserve-env=CI $(GO_ENV) $(CGO_ENV) $(GO) test $(SANITIZERS) ./test/integration/... -count=5

‎bpf/Makefile

+19-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ OUT_BPF := $(OUT_BPF_DIR)/native.bpf.o
1717
OUT_RBPERF := $(OUT_BPF_DIR)/rbperf.bpf.o
1818
OUT_PYPERF := $(OUT_BPF_DIR)/pyperf.bpf.o
1919
OUT_JVM := $(OUT_BPF_DIR)/jvm.bpf.o
20+
OUT_LUA := $(OUT_BPF_DIR)/lua.bpf.o
2021
OUT_PID_NAMESPACE_DETECTOR := $(OUT_BPF_DIR)/pid_namespace.bpf.o
2122
BPF_BUNDLE := $(OUT_DIR)/parca-agent.bpf.tar.gz
2223

@@ -28,6 +29,7 @@ BPF_SRC := unwinders/native.bpf.c unwinders/go_runtime.h
2829
RBPERF_SRC := unwinders/rbperf.bpf.c
2930
PYPERF_SRC := unwinders/pyperf.bpf.c
3031
JVM_SRC := unwinders/jvm.bpf.c
32+
LUA_SRC := unwinders/lua.bpf.c
3133
OUT_PID_NAMESPACE_DETECTOR_SRC := pid_namespace.bpf.c
3234
BPF_INCLUDES := unwinders/
3335

@@ -46,11 +48,11 @@ format: c/fmt
4648

4749
.PHONY: c/fmt
4850
c/fmt:
49-
$(CMD_CLANG_FORMAT) -i --style=file $(BPF_HEADERS) $(BPF_SRC) $(RBPERF_SRC) $(PYPERF_SRC) $(JVM_SRC) $(OUT_PID_NAMESPACE_DETECTOR_SRC)
51+
$(CMD_CLANG_FORMAT) -i --style=file $(BPF_HEADERS) $(BPF_SRC) $(RBPERF_SRC) $(PYPERF_SRC) $(JVM_SRC) $(LUA_SRC) $(OUT_PID_NAMESPACE_DETECTOR_SRC)
5052

5153
.PHONY: format-check
5254
format-check:
53-
$(CMD_CLANG_FORMAT) --dry-run -Werror --style=file $(BPF_HEADERS) $(BPF_SRC) $(RBPERF_SRC) $(PYPERF_SRC) $(JVM_SRC) $(OUT_PID_NAMESPACE_DETECTOR_SRC)
55+
$(CMD_CLANG_FORMAT) --dry-run -Werror --style=file $(BPF_HEADERS) $(BPF_SRC) $(RBPERF_SRC) $(PYPERF_SRC) $(JVM_SRC) $(LUA_SRC) $(OUT_PID_NAMESPACE_DETECTOR_SRC)
5456

5557
# compilation options:
5658
BPF_CFLAGS = -Wno-address-of-packed-member \
@@ -76,7 +78,7 @@ BPF_CFLAGS = -Wno-address-of-packed-member \
7678

7779
# tasks:
7880
.PHONY: clang
79-
clang: $(OUT_BPF) $(OUT_RBPERF) $(OUT_PYPERF) $(OUT_JVM) $(OUT_PID_NAMESPACE_DETECTOR)
81+
clang: $(OUT_BPF) $(OUT_RBPERF) $(OUT_PYPERF) $(OUT_JVM) $(OUT_LUA) $(OUT_PID_NAMESPACE_DETECTOR)
8082

8183
bpf_bundle_dir := $(OUT_DIR)/parca-agent.bpf
8284
$(BPF_BUNDLE): $(BPF_SRC) $(LIBBPF_HEADERS)/bpf $(BPF_HEADERS)
@@ -152,3 +154,17 @@ $(OUT_JVM): $(JVM_SRC) $(LIBBPF_HEADERS) $(BPF_HEADERS) | $(OUT_DIR)
152154
-O2 -emit-llvm -c -g $< -o $(@:.o=.ll)
153155
$(CMD_LLC) -march=bpf -filetype=obj -o $@ $(@:.o=.ll)
154156
rm $(@:.o=.ll)
157+
158+
$(OUT_LUA): $(LUA_SRC) $(LIBBPF_HEADERS) $(BPF_HEADERS) | $(OUT_DIR)
159+
mkdir -p $(OUT_BPF_DIR)
160+
$(CMD_CC) -S \
161+
-D__BPF_TRACING__ \
162+
-D__KERNEL__ \
163+
-D__TARGET_ARCH_$(SHORT_ARCH) \
164+
-I $(VMLINUX_INCLUDE_PATH) \
165+
-I $(LIBBPF_HEADERS) \
166+
-I $(BPF_INCLUDES) \
167+
$(BPF_CFLAGS) \
168+
-O2 -emit-llvm -c -g $< -o $(@:.o=.ll)
169+
$(CMD_LLC) -march=bpf -filetype=obj -o $@ $(@:.o=.ll)
170+
rm $(@:.o=.ll)

‎bpf/unwinders/lj_bc.h

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
** Bytecode instruction format.
3+
** Copyright (C) 2005-2023 Mike Pall. See Copyright Notice in luajit.h
4+
*/
5+
6+
#ifndef _LJ_BC_H
7+
#define _LJ_BC_H
8+
9+
// #include "lj_def.h"
10+
// #include "lj_arch.h"
11+
12+
/* Bytecode instruction format, 32 bit wide, fields of 8 or 16 bit:
13+
**
14+
** +----+----+----+----+
15+
** | B | C | A | OP | Format ABC
16+
** +----+----+----+----+
17+
** | D | A | OP | Format AD
18+
** +--------------------
19+
** MSB LSB
20+
**
21+
** In-memory instructions are always stored in host byte order.
22+
*/
23+
24+
/* Operand ranges and related constants. */
25+
#define BCMAX_A 0xff
26+
#define BCMAX_B 0xff
27+
#define BCMAX_C 0xff
28+
#define BCMAX_D 0xffff
29+
#define BCBIAS_J 0x8000
30+
#define NO_REG BCMAX_A
31+
#define NO_JMP (~(BCPos)0)
32+
33+
/* Macros to get instruction fields. */
34+
#define bc_op(i) ((BCOp)((i) & 0xff))
35+
#define bc_a(i) ((BCReg)(((i) >> 8) & 0xff))
36+
#define bc_b(i) ((BCReg)((i) >> 24))
37+
#define bc_c(i) ((BCReg)(((i) >> 16) & 0xff))
38+
#define bc_d(i) ((BCReg)((i) >> 16))
39+
#define bc_j(i) ((ptrdiff_t)bc_d(i) - BCBIAS_J)
40+
41+
/* Macros to set instruction fields. */
42+
#define setbc_byte(p, x, ofs) ((uint8_t *)(p))[LJ_ENDIAN_SELECT(ofs, 3 - ofs)] = (uint8_t)(x)
43+
#define setbc_op(p, x) setbc_byte(p, (x), 0)
44+
#define setbc_a(p, x) setbc_byte(p, (x), 1)
45+
#define setbc_b(p, x) setbc_byte(p, (x), 3)
46+
#define setbc_c(p, x) setbc_byte(p, (x), 2)
47+
#define setbc_d(p, x) ((uint16_t *)(p))[LJ_ENDIAN_SELECT(1, 0)] = (uint16_t)(x)
48+
#define setbc_j(p, x) setbc_d(p, (BCPos)((int32_t)(x) + BCBIAS_J))
49+
50+
/* Macros to compose instructions. */
51+
#define BCINS_ABC(o, a, b, c) (((BCIns)(o)) | ((BCIns)(a) << 8) | ((BCIns)(b) << 24) | ((BCIns)(c) << 16))
52+
#define BCINS_AD(o, a, d) (((BCIns)(o)) | ((BCIns)(a) << 8) | ((BCIns)(d) << 16))
53+
#define BCINS_AJ(o, a, j) BCINS_AD(o, a, (BCPos)((int32_t)(j) + BCBIAS_J))
54+
55+
/* Bytecode instruction definition. Order matters, see below.
56+
**
57+
** (name, filler, Amode, Bmode, Cmode or Dmode, metamethod)
58+
**
59+
** The opcode name suffixes specify the type for RB/RC or RD:
60+
** V = variable slot
61+
** S = string const
62+
** N = number const
63+
** P = primitive type (~itype)
64+
** B = unsigned byte literal
65+
** M = multiple args/results
66+
*/
67+
#define BCDEF(_) \
68+
/* Comparison ops. ORDER OPR. */ \
69+
_(ISLT, var, ___, var, lt) \
70+
_(ISGE, var, ___, var, lt) \
71+
_(ISLE, var, ___, var, le) \
72+
_(ISGT, var, ___, var, le) \
73+
\
74+
_(ISEQV, var, ___, var, eq) \
75+
_(ISNEV, var, ___, var, eq) \
76+
_(ISEQS, var, ___, str, eq) \
77+
_(ISNES, var, ___, str, eq) \
78+
_(ISEQN, var, ___, num, eq) \
79+
_(ISNEN, var, ___, num, eq) \
80+
_(ISEQP, var, ___, pri, eq) \
81+
_(ISNEP, var, ___, pri, eq) \
82+
\
83+
/* Unary test and copy ops. */ \
84+
_(ISTC, dst, ___, var, ___) \
85+
_(ISFC, dst, ___, var, ___) \
86+
_(IST, ___, ___, var, ___) \
87+
_(ISF, ___, ___, var, ___) \
88+
_(ISTYPE, var, ___, lit, ___) \
89+
_(ISNUM, var, ___, lit, ___) \
90+
\
91+
/* Unary ops. */ \
92+
_(MOV, dst, ___, var, ___) \
93+
_(NOT, dst, ___, var, ___) \
94+
_(UNM, dst, ___, var, unm) \
95+
_(LEN, dst, ___, var, len) \
96+
\
97+
/* Binary ops. ORDER OPR. VV last, POW must be next. */ \
98+
_(ADDVN, dst, var, num, add) \
99+
_(SUBVN, dst, var, num, sub) \
100+
_(MULVN, dst, var, num, mul) \
101+
_(DIVVN, dst, var, num, div) \
102+
_(MODVN, dst, var, num, mod) \
103+
\
104+
_(ADDNV, dst, var, num, add) \
105+
_(SUBNV, dst, var, num, sub) \
106+
_(MULNV, dst, var, num, mul) \
107+
_(DIVNV, dst, var, num, div) \
108+
_(MODNV, dst, var, num, mod) \
109+
\
110+
_(ADDVV, dst, var, var, add) \
111+
_(SUBVV, dst, var, var, sub) \
112+
_(MULVV, dst, var, var, mul) \
113+
_(DIVVV, dst, var, var, div) \
114+
_(MODVV, dst, var, var, mod) \
115+
\
116+
_(POW, dst, var, var, pow) \
117+
_(CAT, dst, rbase, rbase, concat) \
118+
\
119+
/* Constant ops. */ \
120+
_(KSTR, dst, ___, str, ___) \
121+
_(KCDATA, dst, ___, cdata, ___) \
122+
_(KSHORT, dst, ___, lits, ___) \
123+
_(KNUM, dst, ___, num, ___) \
124+
_(KPRI, dst, ___, pri, ___) \
125+
_(KNIL, base, ___, base, ___) \
126+
\
127+
/* Upvalue and function ops. */ \
128+
_(UGET, dst, ___, uv, ___) \
129+
_(USETV, uv, ___, var, ___) \
130+
_(USETS, uv, ___, str, ___) \
131+
_(USETN, uv, ___, num, ___) \
132+
_(USETP, uv, ___, pri, ___) \
133+
_(UCLO, rbase, ___, jump, ___) \
134+
_(FNEW, dst, ___, func, gc) \
135+
\
136+
/* Table ops. */ \
137+
_(TNEW, dst, ___, lit, gc) \
138+
_(TDUP, dst, ___, tab, gc) \
139+
_(GGET, dst, ___, str, index) \
140+
_(GSET, var, ___, str, newindex) \
141+
_(TGETV, dst, var, var, index) \
142+
_(TGETS, dst, var, str, index) \
143+
_(TGETB, dst, var, lit, index) \
144+
_(TGETR, dst, var, var, index) \
145+
_(TSETV, var, var, var, newindex) \
146+
_(TSETS, var, var, str, newindex) \
147+
_(TSETB, var, var, lit, newindex) \
148+
_(TSETM, base, ___, num, newindex) \
149+
_(TSETR, var, var, var, newindex) \
150+
\
151+
/* Calls and vararg handling. T = tail call. */ \
152+
_(CALLM, base, lit, lit, call) \
153+
_(CALL, base, lit, lit, call) \
154+
_(CALLMT, base, ___, lit, call) \
155+
_(CALLT, base, ___, lit, call) \
156+
_(ITERC, base, lit, lit, call) \
157+
_(ITERN, base, lit, lit, call) \
158+
_(VARG, base, lit, lit, ___) \
159+
_(ISNEXT, base, ___, jump, ___) \
160+
\
161+
/* Returns. */ \
162+
_(RETM, base, ___, lit, ___) \
163+
_(RET, rbase, ___, lit, ___) \
164+
_(RET0, rbase, ___, lit, ___) \
165+
_(RET1, rbase, ___, lit, ___) \
166+
\
167+
/* Loops and branches. I/J = interp/JIT, I/C/L = init/call/loop. */ \
168+
_(FORI, base, ___, jump, ___) \
169+
_(JFORI, base, ___, jump, ___) \
170+
\
171+
_(FORL, base, ___, jump, ___) \
172+
_(IFORL, base, ___, jump, ___) \
173+
_(JFORL, base, ___, lit, ___) \
174+
\
175+
_(ITERL, base, ___, jump, ___) \
176+
_(IITERL, base, ___, jump, ___) \
177+
_(JITERL, base, ___, lit, ___) \
178+
\
179+
_(LOOP, rbase, ___, jump, ___) \
180+
_(ILOOP, rbase, ___, jump, ___) \
181+
_(JLOOP, rbase, ___, lit, ___) \
182+
\
183+
_(JMP, rbase, ___, jump, ___) \
184+
\
185+
/* Function headers. I/J = interp/JIT, F/V/C = fixarg/vararg/C func. */ \
186+
_(FUNCF, rbase, ___, ___, ___) \
187+
_(IFUNCF, rbase, ___, ___, ___) \
188+
_(JFUNCF, rbase, ___, lit, ___) \
189+
_(FUNCV, rbase, ___, ___, ___) \
190+
_(IFUNCV, rbase, ___, ___, ___) \
191+
_(JFUNCV, rbase, ___, lit, ___) \
192+
_(FUNCC, rbase, ___, ___, ___) \
193+
_(FUNCCW, rbase, ___, ___, ___)
194+
195+
/* Bytecode opcode numbers. */
196+
typedef enum {
197+
#define BCENUM(name, ma, mb, mc, mt) BC_##name,
198+
BCDEF(BCENUM)
199+
#undef BCENUM
200+
BC__MAX
201+
} BCOp;
202+
203+
LJ_STATIC_ASSERT((int)BC_ISEQV + 1 == (int)BC_ISNEV);
204+
LJ_STATIC_ASSERT(((int)BC_ISEQV ^ 1) == (int)BC_ISNEV);
205+
LJ_STATIC_ASSERT(((int)BC_ISEQS ^ 1) == (int)BC_ISNES);
206+
LJ_STATIC_ASSERT(((int)BC_ISEQN ^ 1) == (int)BC_ISNEN);
207+
LJ_STATIC_ASSERT(((int)BC_ISEQP ^ 1) == (int)BC_ISNEP);
208+
LJ_STATIC_ASSERT(((int)BC_ISLT ^ 1) == (int)BC_ISGE);
209+
LJ_STATIC_ASSERT(((int)BC_ISLE ^ 1) == (int)BC_ISGT);
210+
LJ_STATIC_ASSERT(((int)BC_ISLT ^ 3) == (int)BC_ISGT);
211+
LJ_STATIC_ASSERT((int)BC_IST - (int)BC_ISTC == (int)BC_ISF - (int)BC_ISFC);
212+
LJ_STATIC_ASSERT((int)BC_CALLT - (int)BC_CALL == (int)BC_CALLMT - (int)BC_CALLM);
213+
LJ_STATIC_ASSERT((int)BC_CALLMT + 1 == (int)BC_CALLT);
214+
LJ_STATIC_ASSERT((int)BC_RETM + 1 == (int)BC_RET);
215+
LJ_STATIC_ASSERT((int)BC_FORL + 1 == (int)BC_IFORL);
216+
LJ_STATIC_ASSERT((int)BC_FORL + 2 == (int)BC_JFORL);
217+
LJ_STATIC_ASSERT((int)BC_ITERL + 1 == (int)BC_IITERL);
218+
LJ_STATIC_ASSERT((int)BC_ITERL + 2 == (int)BC_JITERL);
219+
LJ_STATIC_ASSERT((int)BC_LOOP + 1 == (int)BC_ILOOP);
220+
LJ_STATIC_ASSERT((int)BC_LOOP + 2 == (int)BC_JLOOP);
221+
LJ_STATIC_ASSERT((int)BC_FUNCF + 1 == (int)BC_IFUNCF);
222+
LJ_STATIC_ASSERT((int)BC_FUNCF + 2 == (int)BC_JFUNCF);
223+
LJ_STATIC_ASSERT((int)BC_FUNCV + 1 == (int)BC_IFUNCV);
224+
LJ_STATIC_ASSERT((int)BC_FUNCV + 2 == (int)BC_JFUNCV);
225+
226+
/* This solves a circular dependency problem, change as needed. */
227+
#define FF_next_N 4
228+
229+
/* Stack slots used by FORI/FORL, relative to operand A. */
230+
enum { FORL_IDX, FORL_STOP, FORL_STEP, FORL_EXT };
231+
232+
/* Bytecode operand modes. ORDER BCMode */
233+
typedef enum {
234+
BCMnone,
235+
BCMdst,
236+
BCMbase,
237+
BCMvar,
238+
BCMrbase,
239+
BCMuv, /* Mode A must be <= 7 */
240+
BCMlit,
241+
BCMlits,
242+
BCMpri,
243+
BCMnum,
244+
BCMstr,
245+
BCMtab,
246+
BCMfunc,
247+
BCMjump,
248+
BCMcdata,
249+
BCM_max
250+
} BCMode;
251+
#define BCM___ BCMnone
252+
253+
#define bcmode_a(op) ((BCMode)(lj_bc_mode[op] & 7))
254+
#define bcmode_b(op) ((BCMode)((lj_bc_mode[op] >> 3) & 15))
255+
#define bcmode_c(op) ((BCMode)((lj_bc_mode[op] >> 7) & 15))
256+
#define bcmode_d(op) bcmode_c(op)
257+
#define bcmode_hasd(op) ((lj_bc_mode[op] & (15 << 3)) == (BCMnone << 3))
258+
#define bcmode_mm(op) ((MMS)(lj_bc_mode[op] >> 11))
259+
260+
#define BCMODE(name, ma, mb, mc, mm) (BCM##ma | (BCM##mb << 3) | (BCM##mc << 7) | (MM_##mm << 11)),
261+
#define BCMODE_FF 0
262+
263+
static LJ_AINLINE int bc_isret(BCOp op) {
264+
return (op == BC_RETM || op == BC_RET || op == BC_RET0 || op == BC_RET1);
265+
}
266+
267+
LJ_DATA const uint16_t lj_bc_mode[];
268+
LJ_DATA const uint16_t lj_bc_ofs[];
269+
270+
#endif

‎bpf/unwinders/lj_bcdef.h

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* This is a generated file. DO NOT EDIT! */
2+
3+
LJ_DATADEF const uint16_t lj_bc_ofs[] = {
4+
0, 92, 184, 276, 368, 518, 671, 751, 831, 909, 987, 1047, 1106, 1162, 1218, 1267, 1316, 1351, 1388, 1415, 1465, 1522,
5+
1613, 1672, 1731, 1790, 1849, 1913, 1972, 2031, 2090, 2149, 2189, 2265, 2341, 2417, 2493, 2550, 2641, 2725, 2768, 2811, 2842, 2872,
6+
2902, 2953, 3001, 3113, 3217, 3267, 3317, 3376, 3461, 3583, 3689, 3717, 3745, 3910, 4056, 4161, 4226, 4421, 4673, 4808, 4970, 5065,
7+
5129, 5190, 5193, 5383, 5468, 5645, 5854, 5997, 6000, 6150, 6257, 6378, 6493, 6616, 6636, 6711, 6782, 6802, 6846, 6884, 6904, 6923,
8+
6973, 7000, 7020, 7088, 7146, 7146, 7281, 7282, 7392, 9537, 9612, 10234, 10333, 10431, 10596, 9683, 9867, 9992, 10065, 10107, 10694, 10777,
9+
11557, 10850, 11237, 11605, 11768, 11796, 11650, 11873, 11919, 11965, 12011, 12057, 12103, 12149, 12195, 12241, 12287, 12333, 12651, 12727, 11827, 12448,
10+
12379, 12517, 12586, 12791, 12868, 13646, 14082, 14025, 14154, 14242, 14333, 14424, 14515, 13704, 13811, 13918, 12945, 13009, 13143, 13322, 13430, 13538
11+
};
12+
13+
LJ_DATADEF const uint16_t lj_bc_mode[] = {
14+
BCDEF(BCMODE) BCMODE_FF,
15+
BCMODE_FF,
16+
BCMODE_FF,
17+
BCMODE_FF,
18+
BCMODE_FF,
19+
BCMODE_FF,
20+
BCMODE_FF,
21+
BCMODE_FF,
22+
BCMODE_FF,
23+
BCMODE_FF,
24+
BCMODE_FF,
25+
BCMODE_FF,
26+
BCMODE_FF,
27+
BCMODE_FF,
28+
BCMODE_FF,
29+
BCMODE_FF,
30+
BCMODE_FF,
31+
BCMODE_FF,
32+
BCMODE_FF,
33+
BCMODE_FF,
34+
BCMODE_FF,
35+
BCMODE_FF,
36+
BCMODE_FF,
37+
BCMODE_FF,
38+
BCMODE_FF,
39+
BCMODE_FF,
40+
BCMODE_FF,
41+
BCMODE_FF,
42+
BCMODE_FF,
43+
BCMODE_FF,
44+
BCMODE_FF,
45+
BCMODE_FF,
46+
BCMODE_FF,
47+
BCMODE_FF,
48+
BCMODE_FF,
49+
BCMODE_FF,
50+
BCMODE_FF,
51+
BCMODE_FF,
52+
BCMODE_FF,
53+
BCMODE_FF,
54+
BCMODE_FF,
55+
BCMODE_FF,
56+
BCMODE_FF,
57+
BCMODE_FF,
58+
BCMODE_FF,
59+
BCMODE_FF,
60+
BCMODE_FF,
61+
BCMODE_FF,
62+
BCMODE_FF,
63+
BCMODE_FF,
64+
BCMODE_FF,
65+
BCMODE_FF,
66+
BCMODE_FF,
67+
BCMODE_FF,
68+
BCMODE_FF,
69+
BCMODE_FF,
70+
BCMODE_FF
71+
};

‎bpf/unwinders/lua.bpf.c

+671
Large diffs are not rendered by default.

‎bpf/unwinders/lua.h

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// SPDX-License-Identifier: GPL-2.0-only
2+
// Copyright 2022 The Parca Authors

‎bpf/unwinders/lua_state.h

+1,557
Large diffs are not rendered by default.

‎bpf/unwinders/native.bpf.c

+21-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#define RUBY_UNWINDER_PROGRAM_ID 1
2323
#define PYTHON_UNWINDER_PROGRAM_ID 2
2424
#define JAVA_UNWINDER_PROGRAM_ID 3
25+
#define LUA_UNWINDER_PROGRAM_ID 4
2526

2627
#if __TARGET_ARCH_x86
2728
// Number of frames to walk per tail call iteration.
@@ -104,6 +105,7 @@ enum runtime_unwinder_type {
104105
RUNTIME_UNWINDER_TYPE_PYTHON = 2,
105106
RUNTIME_UNWINDER_TYPE_JAVA = 3,
106107
RUNTIME_UNWINDER_TYPE_GO = 4,
108+
RUNTIME_UNWINDER_TYPE_LUA = 5,
107109
};
108110

109111
enum find_unwind_table_return {
@@ -126,8 +128,7 @@ struct unwinder_config_t {
126128
bool ruby_enabled;
127129
bool java_enabled;
128130
bool collect_trace_id;
129-
/* 1 byte of padding */
130-
bool _padding;
131+
bool lua_enabled;
131132
u32 rate_limit_unwind_info;
132133
u32 rate_limit_process_mappings;
133134
u32 rate_limit_refresh_process_info;
@@ -301,7 +302,7 @@ struct {
301302

302303
struct {
303304
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
304-
__uint(max_entries, 4);
305+
__uint(max_entries, 5);
305306
__type(key, u32);
306307
__type(value, u32);
307308
} programs SEC(".maps");
@@ -848,6 +849,15 @@ static __always_inline void add_stack(struct bpf_perf_event_data *ctx, u64 pid_t
848849
LOG("[debug] tail-call to Java unwinder (jvm)");
849850
bpf_tail_call(ctx, &programs, JAVA_UNWINDER_PROGRAM_ID);
850851
break;
852+
case RUNTIME_UNWINDER_TYPE_LUA:
853+
if (!unwinder_config.lua_enabled) {
854+
LOG("[debug] Lua unwinder is disabled");
855+
aggregate_stacks();
856+
break;
857+
}
858+
LOG("[debug] tail-call to Lua unwinder");
859+
bpf_tail_call(ctx, &programs, LUA_UNWINDER_PROGRAM_ID);
860+
break;
851861
default:
852862
LOG("[error] bad runtime unwinder type value: %d", unwind_state->unwinder_type);
853863
break;
@@ -1402,7 +1412,7 @@ int entrypoint(struct bpf_perf_event_data *ctx) {
14021412
if (!is_debug_enabled_for_thread(per_process_id)) {
14031413
bump_unwind_total_filter_misses();
14041414
BUMP_UNWIND_FAILED_COUNT(per_process_id, missed_filter);
1405-
LOG("[debug] pid %u didn't match filter, ignoring.", per_process_id);
1415+
// LOG("[debug] pid %u didn't match filter, ignoring.", per_process_id);
14061416
return 0;
14071417
} else {
14081418
LOG("[debug] pid %u matched filter.", per_process_id);
@@ -1451,6 +1461,13 @@ int entrypoint(struct bpf_perf_event_data *ctx) {
14511461
BUMP_UNWIND_FAILED_COUNT(per_process_id, pc_not_covered);
14521462
return 1;
14531463
}
1464+
if (unwind_state->unwinder_type == RUNTIME_UNWINDER_TYPE_LUA) {
1465+
LOG("[info] LUA JIT pc 0x%llx encountered, tail calling lua unwinder", unwind_state->ip);
1466+
// Set this bit to inform the Lua unwinder to attempt to get the line number of the top frame.
1467+
unwind_state->unwinding_jit = true;
1468+
bpf_tail_call(ctx, &programs, LUA_UNWINDER_PROGRAM_ID);
1469+
return 0;
1470+
}
14541471
} else if (proc_info->is_jit_compiler) {
14551472
LOG("[warn] IP 0x%llx not covered, may be JIT!.", unwind_state->ip);
14561473
request_refresh_process_info(ctx, per_process_id);

‎cmd/parca-agent/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ type flags struct {
144144
PythonUnwindingDisable bool `default:"false" help:"Disable Python unwinder."`
145145
RubyUnwindingDisable bool `default:"false" help:"Disable Ruby unwinder."`
146146
JavaUnwindingDisable bool `default:"true" help:"Disable Java unwinder."`
147+
LuaUnwindingDisable bool `default:"false" help:"Disable Lua unwinder."`
147148

148149
CollectTraceID bool `default:"false" help:"Attempt to collect trace ID from the process."`
149150

@@ -966,6 +967,7 @@ func run(logger log.Logger, reg *prometheus.Registry, flags flags, cpus cpuinfo.
966967
PythonUnwindingEnabled: !flags.PythonUnwindingDisable,
967968
RubyUnwindingEnabled: !flags.RubyUnwindingDisable,
968969
JavaUnwindingEnabled: !flags.JavaUnwindingDisable,
970+
LuaUnwindingEnabled: !flags.LuaUnwindingDisable,
969971
RateLimitUnwindInfo: flags.Hidden.RateLimitUnwindInfo,
970972
RateLimitProcessMappings: flags.Hidden.RateLimitProcessMappings,
971973
RateLimitRefreshProcessInfo: flags.Hidden.RateLimitRefreshProcessInfo,

‎pkg/pprof/pprof.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"context"
1818
"encoding/hex"
1919
"errors"
20+
"fmt"
2021
"io/fs"
2122
"strconv"
2223
"time"
@@ -369,7 +370,7 @@ func (c *Converter) addKernelLocation(
369370
func (c *Converter) interpreterSymbol(frameID uint32) *profile.Function {
370371
interpreterSymbol, ok := c.interpreterSymbolTable[frameID]
371372
if !ok {
372-
return &profile.Function{Name: "<not found>"}
373+
return &profile.Function{Name: fmt.Sprintf("<not found>:%x", frameID)}
373374
}
374375
return interpreterSymbol
375376
}

‎pkg/process/info.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,15 @@ func NewInfoManager(
158158
metrics: newMetrics(reg),
159159
cache: cache.NewLRUCacheWithTTL[int, Info](
160160
prometheus.WrapRegistererWith(prometheus.Labels{"cache": "process_info"}, reg),
161-
1024,
161+
10240,
162162
12*profilingDuration,
163163
cache.CacheWithTTLOptions{
164164
RemoveExpiredOnAdd: true,
165165
},
166166
),
167167
cacheForMappings: cache.NewLRUCacheWithTTL[int, uint64](
168168
prometheus.WrapRegistererWith(prometheus.Labels{"cache": "process_mapping_info"}, reg),
169-
1024,
169+
10240,
170170
cacheTTL,
171171
),
172172
shouldInitiateUploadCache: cache.NewLRUCacheWithTTL[string, struct{}](

‎pkg/profiler/cpu/bpf/maps/maps.go

+120-5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import (
6565
runtimego "github.com/parca-dev/parca-agent/pkg/runtime/golang"
6666
runtimejava "github.com/parca-dev/parca-agent/pkg/runtime/java"
6767
runtimelibc "github.com/parca-dev/parca-agent/pkg/runtime/libc"
68+
"github.com/parca-dev/parca-agent/pkg/runtime/lua"
6869
runtimepython "github.com/parca-dev/parca-agent/pkg/runtime/python"
6970
runtimeruby "github.com/parca-dev/parca-agent/pkg/runtime/ruby"
7071
"github.com/parca-dev/parca-agent/pkg/stack/unwind"
@@ -97,6 +98,9 @@ const (
9798
// native runtime info maps
9899
NativePIDToRuntimeInfoMapName = "pid_to_runtime_info"
99100

101+
// Lua maps.
102+
LuaTIDToStateAddress = "tid_to_state"
103+
100104
UnwindInfoChunksMapName = "unwind_info_chunks"
101105
UnwindTablesMapName = "unwind_tables"
102106
ProcessInfoMapName = "process_info"
@@ -197,6 +201,9 @@ type Maps struct {
197201
rbperfModule *libbpf.Module
198202
pyperfModule *libbpf.Module
199203
jvmModule *libbpf.Module
204+
luaModule *libbpf.Module
205+
206+
disableUProbes bool
200207

201208
debugPIDs *libbpf.BPFMap
202209

@@ -215,6 +222,7 @@ type Maps struct {
215222
javaVersionSpecificOffsets *libbpf.BPFMap
216223

217224
nativePIDToRuntimeInfo *libbpf.BPFMap
225+
luaTIDToState *libbpf.BPFMap
218226

219227
// Keeps track of synced process unwinder info.
220228
syncedUnwinders *cache.Cache[int, runtime.UnwinderInfo]
@@ -287,6 +295,7 @@ const (
287295
RbperfModule
288296
PyperfModule
289297
JVMModule
298+
LuaModule
290299
)
291300

292301
type stackTraceWithLength struct {
@@ -328,6 +337,7 @@ func New(
328337
rbperfModule: modules[RbperfModule],
329338
pyperfModule: modules[PyperfModule],
330339
jvmModule: modules[JVMModule],
340+
luaModule: modules[LuaModule],
331341
byteOrder: binary.LittleEndian,
332342
processCache: processCache,
333343
mappingInfoMemory: mappingInfoMemory,
@@ -347,8 +357,12 @@ func New(
347357
return maps, nil
348358
}
349359

360+
func (m *Maps) InterpretersActive() bool {
361+
return m.pyperfModule != nil || m.rbperfModule != nil || m.jvmModule != nil || m.luaModule != nil
362+
}
363+
350364
func (m *Maps) ReuseMaps() error {
351-
if m.pyperfModule == nil && m.rbperfModule == nil && m.jvmModule == nil {
365+
if !m.InterpretersActive() {
352366
return nil
353367
}
354368

@@ -512,6 +526,51 @@ func (m *Maps) ReuseMaps() error {
512526
}
513527
}
514528

529+
if m.luaModule != nil {
530+
// Fetch lua maps.
531+
luaHeap, err := m.luaModule.GetMap(heapMapName)
532+
if err != nil {
533+
return fmt.Errorf("get map (lua) heap: %w", err)
534+
}
535+
luaStackCounts, err := m.luaModule.GetMap(StackCountsMapName)
536+
if err != nil {
537+
return fmt.Errorf("get map (lua) stack_counts: %w", err)
538+
}
539+
luaStackTraces, err := m.luaModule.GetMap(StackTracesMapName)
540+
if err != nil {
541+
return fmt.Errorf("get map (lua) stack_traces: %w", err)
542+
}
543+
luaSymbolIndex, err := m.luaModule.GetMap(symbolIndexStorageMapName)
544+
if err != nil {
545+
return fmt.Errorf("get map (lua) symbol_index_storage: %w", err)
546+
}
547+
luaSymbolTable, err := m.luaModule.GetMap(symbolTableMapName)
548+
if err != nil {
549+
return fmt.Errorf("get map (lua) symbol_table: %w", err)
550+
}
551+
552+
// Reuse maps across programs.
553+
err = luaHeap.ReuseFD(heapNative.FileDescriptor())
554+
if err != nil {
555+
return fmt.Errorf("reuse map (lua) heap: %w", err)
556+
}
557+
err = luaStackCounts.ReuseFD(stackCountNative.FileDescriptor())
558+
if err != nil {
559+
return fmt.Errorf("reuse map (lua) stack_counts: %w", err)
560+
}
561+
err = luaStackTraces.ReuseFD(stackTracesNative.FileDescriptor())
562+
if err != nil {
563+
return fmt.Errorf("reuse map (lua) stack_traces: %w", err)
564+
}
565+
err = luaSymbolIndex.ReuseFD(symbolIndexStorage.FileDescriptor())
566+
if err != nil {
567+
return fmt.Errorf("reuse map (lua) symbol_index_storage: %w", err)
568+
}
569+
err = luaSymbolTable.ReuseFD(symbolTableMap.FileDescriptor())
570+
if err != nil {
571+
return fmt.Errorf("reuse map (lua) symbol_table: %w", err)
572+
}
573+
}
515574
return nil
516575
}
517576

@@ -792,7 +851,7 @@ func (m *Maps) setMuslOffsets(offsets map[runtimedata.Key]*libc.Layout) error {
792851
}
793852

794853
func (m *Maps) SetUnwinderData() error {
795-
if m.pyperfModule == nil && m.rbperfModule == nil && m.jvmModule == nil {
854+
if !m.InterpretersActive() {
796855
return nil
797856
}
798857

@@ -853,7 +912,7 @@ func (m *Maps) SetUnwinderData() error {
853912
}
854913

855914
func (m *Maps) UpdateTailCallsMap() error {
856-
if m.pyperfModule == nil && m.rbperfModule == nil && m.jvmModule == nil {
915+
if !m.InterpretersActive() {
857916
return nil
858917
}
859918

@@ -946,6 +1005,33 @@ func (m *Maps) UpdateTailCallsMap() error {
9461005
}
9471006
}
9481007

1008+
if m.luaModule != nil {
1009+
luaEntrypointProg, err := m.luaModule.GetProgram("unwind_lua_stack")
1010+
if err != nil {
1011+
return fmt.Errorf("get program unwind_lua_stack: %w", err)
1012+
}
1013+
1014+
luaEntrypointFD := luaEntrypointProg.FileDescriptor()
1015+
if err = entrypointPrograms.Update(unsafe.Pointer(&bpfprograms.LuaEntrypointProgramFD), unsafe.Pointer(&luaEntrypointFD)); err != nil {
1016+
return fmt.Errorf("update (native) programs: %w", err)
1017+
}
1018+
1019+
luaWalkerProg, err := m.luaModule.GetProgram("walk_lua_stack")
1020+
if err != nil {
1021+
return fmt.Errorf("get program walk_lua_stack: %w", err)
1022+
}
1023+
1024+
luaPrograms, err := m.luaModule.GetMap(ProgramsMapName)
1025+
if err != nil {
1026+
return fmt.Errorf("get map (lua) programs: %w", err)
1027+
}
1028+
1029+
luaWalkerFD := luaWalkerProg.FileDescriptor()
1030+
if err = luaPrograms.Update(unsafe.Pointer(&bpfprograms.LuaUnwinderProgramFD), unsafe.Pointer(&luaWalkerFD)); err != nil {
1031+
return fmt.Errorf("update (lua) programs: %w", err)
1032+
}
1033+
}
1034+
9491035
return nil
9501036
}
9511037

@@ -971,7 +1057,7 @@ func (m *Maps) AdjustMapSizes(debugEnabled bool, unwindTableShards, eventsBuffer
9711057

9721058
m.maxUnwindShards = uint64(unwindTableShards)
9731059

974-
if m.pyperfModule != nil || m.rbperfModule != nil || m.jvmModule != nil {
1060+
if m.InterpretersActive() {
9751061
symbolTable, err := m.nativeModule.GetMap(symbolTableMapName)
9761062
if err != nil {
9771063
return fmt.Errorf("get symbol table map: %w", err)
@@ -1061,7 +1147,7 @@ func (m *Maps) Create() error {
10611147
m.unwindFailedReasons = unwindFailedReasons
10621148
m.nativePIDToRuntimeInfo = nativePIDToRuntimeInfo
10631149

1064-
if m.pyperfModule == nil && m.rbperfModule == nil && m.jvmModule == nil {
1150+
if !m.InterpretersActive() {
10651151
return nil
10661152
}
10671153

@@ -1120,6 +1206,15 @@ func (m *Maps) Create() error {
11201206
m.javaVersionSpecificOffsets = javaVersionSpecificOffsets
11211207
}
11221208

1209+
if m.luaModule != nil {
1210+
luaTIDToState, err := m.luaModule.GetMap(LuaTIDToStateAddress)
1211+
if err != nil {
1212+
return fmt.Errorf("get pid to process info map: %w", err)
1213+
}
1214+
1215+
m.luaTIDToState = luaTIDToState
1216+
}
1217+
11231218
return nil
11241219
}
11251220

@@ -1216,6 +1311,24 @@ func (m *Maps) AddUnwinderInfo(pid int, unwinderInfo runtime.UnwinderInfo) error
12161311
return err
12171312
}
12181313
m.syncedUnwinders.Add(pid, unwinderInfo)
1314+
case runtime.UnwinderLua:
1315+
luaUnwinderInfo := unwinderInfo.(*lua.Info) //nolint:forcetypeassert
1316+
m.syncedUnwinders.Add(pid, unwinderInfo)
1317+
if m.luaModule != nil /*& !m.disableUProbes*/ {
1318+
uprobe, err := m.luaModule.GetProgram("lua_entrypoint")
1319+
if err != nil {
1320+
return err
1321+
}
1322+
_, err = uprobe.AttachUprobe(-1, luaUnwinderInfo.Path, uint32(luaUnwinderInfo.PcallOffset))
1323+
if err != nil {
1324+
return err
1325+
}
1326+
_, err = uprobe.AttachUprobe(-1, luaUnwinderInfo.Path, uint32(luaUnwinderInfo.ResumeOffset))
1327+
if err != nil {
1328+
return err
1329+
}
1330+
level.Info(m.logger).Log("msg", "LUA uprobes attached", "pid", pid, "path", luaUnwinderInfo.Path)
1331+
}
12191332
default:
12201333
return fmt.Errorf("invalid interpreter name: %d", typ)
12211334
}
@@ -1250,6 +1363,8 @@ func (m *Maps) indexForUnwinderInfo(unwinderInfo runtime.UnwinderInfo) (uint32,
12501363
return uint32(k.Index), nil
12511364
case runtime.UnwinderGo:
12521365
return 0, nil
1366+
case runtime.UnwinderLua:
1367+
return 0, nil
12531368
default:
12541369
return 0, fmt.Errorf("invalid unwinder type: %d", typ)
12551370
}

‎pkg/profiler/cpu/bpf/programs/programs.go

+7
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ var (
3434
RbperfEntrypointProgramFD = uint64(1)
3535
PyperfEntrypointProgramFD = uint64(2)
3636
JVMEntrypointProgramFD = uint64(3)
37+
LuaEntrypointProgramFD = uint64(4)
3738

3839
// rbperf programs.
3940
RubyUnwinderProgramFD = uint64(0)
4041
// pyperf programs.
4142
PythonUnwinderProgramFD = uint64(0)
4243
// jvm programs.
4344
JavaUnwinderProgramFD = uint64(0)
45+
// lua programs.
46+
LuaUnwinderProgramFD = uint64(0)
4447

4548
ProgramName = "entrypoint"
4649
NativeUnwinderProgramName = "native_unwind"
@@ -64,6 +67,10 @@ func OpenJVM() ([]byte, error) {
6467
return open(fmt.Sprintf("objects/%s/jvm.bpf.o", runtime.GOARCH))
6568
}
6669

70+
func OpenLua() ([]byte, error) {
71+
return open(fmt.Sprintf("objects/%s/lua.bpf.o", runtime.GOARCH))
72+
}
73+
6774
func open(file string) ([]byte, error) {
6875
f, err := objects.Open(file)
6976
if err != nil {

‎pkg/profiler/cpu/cpu.go

+42-8
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ type UnwinderConfig struct {
7272
RubyEnabled bool
7373
JavaEnabled bool
7474
CollectTraceID bool
75-
Padding bool
75+
LuaEnabled bool
7676
RateLimitUnwindInfo uint32
7777
RateLimitProcessMappings uint32
7878
RateLimitRefreshProcessInfo uint32
@@ -99,6 +99,7 @@ type Config struct {
9999
PythonUnwindingEnabled bool
100100
RubyUnwindingEnabled bool
101101
JavaUnwindingEnabled bool
102+
LuaUnwindingEnabled bool
102103

103104
RateLimitUnwindInfo uint32
104105
RateLimitProcessMappings uint32
@@ -257,6 +258,7 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit
257258
rbperf *libbpf.Module
258259
pyperf *libbpf.Module
259260
jvm *libbpf.Module
261+
lua *libbpf.Module
260262
)
261263
if config.RubyUnwindingEnabled {
262264
// rbperf
@@ -309,6 +311,22 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit
309311
level.Info(logger).Log("msg", "loaded jvm BPF module")
310312
}
311313

314+
if config.LuaUnwindingEnabled {
315+
luaBPFObj, err := bpfprograms.OpenLua()
316+
if err != nil {
317+
return nil, nil, err
318+
}
319+
320+
lua, err = libbpf.NewModuleFromBufferArgs(libbpf.NewModuleArgs{
321+
BPFObjBuff: luaBPFObj,
322+
BPFObjName: "parca-lua",
323+
})
324+
if err != nil {
325+
return nil, nil, fmt.Errorf("new bpf module: %w", err)
326+
}
327+
level.Info(logger).Log("msg", "loaded lua BPF module")
328+
}
329+
312330
bpfmapsProcessCache := bpfmaps.NewProcessCache(logger, reg)
313331
syncedUnwinderInfo := cache.NewLRUCache[int, runtime.UnwinderInfo](
314332
prometheus.WrapRegistererWith(prometheus.Labels{"cache": "synced_unwinder_info"}, reg),
@@ -337,6 +355,7 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit
337355
bpfmaps.RbperfModule: rbperf,
338356
bpfmaps.PyperfModule: pyperf,
339357
bpfmaps.JVMModule: jvm,
358+
bpfmaps.LuaModule: lua,
340359
}
341360

342361
// Maps must be initialized before loading the BPF code.
@@ -374,7 +393,7 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit
374393
RubyEnabled: config.RubyUnwindingEnabled,
375394
JavaEnabled: config.JavaUnwindingEnabled,
376395
CollectTraceID: config.CollectTraceID,
377-
Padding: false,
396+
LuaEnabled: config.LuaUnwindingEnabled,
378397
RateLimitUnwindInfo: config.RateLimitUnwindInfo,
379398
RateLimitProcessMappings: config.RateLimitProcessMappings,
380399
RateLimitRefreshProcessInfo: config.RateLimitRefreshProcessInfo,
@@ -401,6 +420,12 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit
401420
}
402421
}
403422

423+
if config.LuaUnwindingEnabled {
424+
if err := lua.InitGlobalVariable("verbose", config.BPFVerboseLoggingEnabled); err != nil {
425+
return nil, nil, fmt.Errorf("lua: init global variable: %w", err)
426+
}
427+
}
428+
404429
level.Debug(logger).Log("msg", "loading BPF object for native unwinder")
405430
lerr = native.BPFLoadObject()
406431
if lerr == nil {
@@ -434,6 +459,14 @@ func loadBPFModules(logger log.Logger, reg prometheus.Registerer, memlockRlimit
434459
}
435460
}
436461

462+
if config.LuaUnwindingEnabled {
463+
level.Debug(logger).Log("msg", "loading BPF object for Lua unwinder")
464+
err = lua.BPFLoadObject()
465+
if err != nil {
466+
return nil, nil, fmt.Errorf("failed to load lua: %w", err)
467+
}
468+
}
469+
437470
level.Debug(logger).Log("msg", "updating programs map")
438471
err = bpfMaps.UpdateTailCallsMap()
439472
if err != nil {
@@ -1079,14 +1112,14 @@ func (p *CPU) Run(ctx context.Context) error {
10791112
pi, err := p.processInfoManager.Info(ctx, pid)
10801113
if err != nil {
10811114
p.metrics.profileDrop.WithLabelValues(labelProfileDropReasonProcessInfo).Inc()
1082-
level.Debug(p.logger).Log("msg", "failed to get process info", "pid", pid, "err", err)
1115+
level.Debug(p.logger).Log("msg", "failed to get process info throwing away samples", "pid", pid, "err", err)
10831116
processLastErrors[pid] = err
10841117
continue
10851118
}
10861119

10871120
interpreterSymbolTable, err := p.interpreterSymbolTable(perProcessRawData.RawSamples)
10881121
if err != nil {
1089-
level.Debug(p.logger).Log("msg", "failed to get interpreter symbol table", "pid", pid, "err", err)
1122+
level.Warn(p.logger).Log("msg", "failed to get interpreter symbol table", "pid", pid, "err", err)
10901123
}
10911124
pprof, executableInfos, err := p.profileConverter.NewConverter(
10921125
pfs,
@@ -1165,7 +1198,7 @@ type profileKey struct {
11651198

11661199
// interpreterSymbolTable returns an up-to-date symbol table for the interpreter.
11671200
func (p *CPU) interpreterSymbolTable(samples []profile.RawSample) (profile.InterpreterSymbolTable, error) {
1168-
if !p.config.RubyUnwindingEnabled && !p.config.PythonUnwindingEnabled && !p.config.JavaUnwindingEnabled {
1201+
if !p.bpfMaps.InterpretersActive() {
11691202
return nil, nil
11701203
}
11711204

@@ -1206,7 +1239,7 @@ func (p *CPU) updateInterpreterSymbolTable() error {
12061239
return nil
12071240
}
12081241

1209-
// obtainProfiles collects profiles from the BPF maps.
1242+
// obtainRawData collects profiles from the BPF maps.
12101243
func (p *CPU) obtainRawData(ctx context.Context) (profile.RawData, map[int]profiler.UnwindFailedReasons, error) {
12111244
rawData := map[profileKey]map[bpfprograms.CombinedStack]uint64{}
12121245

@@ -1257,8 +1290,9 @@ func (p *CPU) obtainRawData(ctx context.Context) (profile.RawData, map[int]profi
12571290
p.metrics.readMapAttempts.WithLabelValues(labelUser, labelNativeUnwind, labelSuccess).Inc()
12581291
}
12591292

1293+
var interpErr error
12601294
if key.InterpreterStackID != 0 {
1261-
if interpErr := p.bpfMaps.ReadStack(key.InterpreterStackID, interpreterStack); interpErr != nil {
1295+
if interpErr = p.bpfMaps.ReadStack(key.InterpreterStackID, interpreterStack); interpErr != nil {
12621296
p.metrics.readMapAttempts.WithLabelValues(labelInterpreter, labelInterpreterUnwind, labelError).Inc()
12631297
level.Debug(p.logger).Log("msg", "failed to read interpreter stacks", "err", interpErr)
12641298
} else {
@@ -1284,7 +1318,7 @@ func (p *CPU) obtainRawData(ctx context.Context) (profile.RawData, map[int]profi
12841318
p.metrics.readMapAttempts.WithLabelValues(labelKernel, labelKernelUnwind, labelSuccess).Inc()
12851319
}
12861320

1287-
if userErr != nil && kernelErr != nil {
1321+
if userErr != nil && kernelErr != nil && (key.InterpreterStackID == 0 || interpErr != nil) {
12881322
// Both user stack (either via frame pointers or dwarf) and kernel stack
12891323
// have failed. Nothing to do.
12901324
continue

‎pkg/runtime/lua/lua.go

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2022-2024 The Parca Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package lua
15+
16+
import (
17+
"debug/elf"
18+
"errors"
19+
"os"
20+
"path"
21+
"strconv"
22+
"strings"
23+
"syscall"
24+
25+
"github.com/prometheus/procfs"
26+
"golang.org/x/exp/mmap"
27+
28+
"github.com/parca-dev/parca-agent/pkg/runtime"
29+
)
30+
31+
func IsRuntime(proc procfs.Proc) (bool, error) {
32+
// flesh out...
33+
exe, err := proc.Executable()
34+
if err != nil {
35+
return false, err
36+
}
37+
if strings.Contains(exe, "openresty") && strings.HasSuffix(exe, "nginx") {
38+
return true, nil
39+
}
40+
if strings.Contains(exe, "luajit") {
41+
return true, nil
42+
}
43+
// TODO: what are other popular lua embedders? We could also just can mappings for libluajit.so.
44+
return false, nil
45+
}
46+
47+
type Info struct {
48+
rt runtime.Runtime
49+
rtType runtime.UnwinderType
50+
// Full path to elf object we'll attach uprobe to, ie /usr/bin/luajit or /usr/lib/libluajit.so.
51+
Path string
52+
PcallOffset uint
53+
ResumeOffset uint
54+
// yield??
55+
}
56+
57+
func (i *Info) Type() runtime.UnwinderType {
58+
return i.rtType
59+
}
60+
61+
func (i *Info) Runtime() runtime.Runtime {
62+
return i.rt
63+
}
64+
65+
// findOldestParentInFS finds the "root" pid that can access the base
66+
// path, for the host this would be the root pid (ie 1 systemd) for
67+
// docker this would be the docker entrypoint CMD. Its helpful to
68+
// use the oldest parent in case the process we're trying to profile
69+
// is short lived and attaching uprobes with /proc/<PID>/root paths fails.
70+
func findOldestParentInFS(pid int, base string) (int, error) {
71+
proc, err := procfs.NewProc(pid)
72+
if err != nil {
73+
return -1, err
74+
}
75+
stat, err := proc.Stat()
76+
if err != nil {
77+
return -1, err
78+
}
79+
80+
myPath := path.Join("/proc/", strconv.Itoa(pid), "/root/", base)
81+
parentPath := path.Join("/proc/", strconv.Itoa(stat.PPID), "/root/", base)
82+
83+
myFileInfo, err := os.Stat(myPath)
84+
if err != nil {
85+
return -1, err
86+
}
87+
parentFileInfo, err := os.Stat(parentPath)
88+
if err != nil {
89+
// If parent cant see that path we're done.
90+
// nolint: nilerr
91+
return pid, nil
92+
}
93+
94+
mySys, ok := myFileInfo.Sys().(*syscall.Stat_t)
95+
if !ok {
96+
return -1, nil
97+
}
98+
parentSys, ok := parentFileInfo.Sys().(*syscall.Stat_t)
99+
if !ok {
100+
return -1, nil
101+
}
102+
103+
if mySys.Ino == parentSys.Ino {
104+
return findOldestParentInFS(stat.PPID, base)
105+
}
106+
return pid, nil
107+
}
108+
109+
func findMappingContains(p procfs.Proc, name string) (string, error) {
110+
maps, err := p.ProcMaps()
111+
if err != nil {
112+
return "", err
113+
}
114+
for _, m := range maps {
115+
if strings.Contains(m.Pathname, name) {
116+
return m.Pathname, nil
117+
}
118+
}
119+
return "", nil
120+
}
121+
122+
func VMInfo(p procfs.Proc) (*Info, error) {
123+
exe, err := findMappingContains(p, "luajit")
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
pid, err := findOldestParentInFS(p.PID, exe)
129+
if err != nil {
130+
return nil, err
131+
}
132+
path := path.Join("/proc/", strconv.Itoa(pid), "/root/", exe)
133+
134+
info := Info{
135+
rt: runtime.Runtime{
136+
Name: "lua",
137+
Version: "1.0.0", // TODO: should we test 5.1 and 5.2 or luajit 2.0/2.1?
138+
VersionSource: "1.0.0",
139+
},
140+
rtType: runtime.UnwinderLua,
141+
Path: path,
142+
}
143+
144+
r, err := mmap.Open(path)
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
e, err := elf.NewFile(r)
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
sym, err := runtime.FindSymbol(e, "lua_pcall")
155+
if err != nil {
156+
return nil, err
157+
}
158+
info.PcallOffset = uint(sym.Value)
159+
160+
sym, err = runtime.FindSymbol(e, "lua_resume")
161+
if err != nil {
162+
return nil, err
163+
}
164+
info.ResumeOffset = uint(sym.Value)
165+
166+
if info.PcallOffset == 0 || info.ResumeOffset == 0 {
167+
return nil, errors.New("unable to locate lua entrypoints for uprobes, lua profiling will be disabled")
168+
}
169+
170+
return &info, nil
171+
}

‎pkg/runtime/lua/lua_probe_bpfeb.go

+119
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/runtime/lua/lua_probe_bpfeb.o

2.38 KB
Binary file not shown.

‎pkg/runtime/lua/lua_probe_bpfel.go

+119
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/runtime/lua/lua_probe_bpfel.o

2.38 KB
Binary file not shown.

‎pkg/runtime/runtime.go

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
UnwinderPython
2222
UnwinderJava
2323
UnwinderGo
24+
UnwinderLua
2425
)
2526

2627
func (it UnwinderType) String() string {
@@ -35,6 +36,8 @@ func (it UnwinderType) String() string {
3536
return "Java"
3637
case UnwinderGo:
3738
return "Go"
39+
case UnwinderLua:
40+
return "Lua"
3841
default:
3942
return "<no string found>"
4043
}

‎pkg/runtime/unwinderinfo/unwinderinfo.go

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/parca-dev/parca-agent/pkg/runtime"
2323
"github.com/parca-dev/parca-agent/pkg/runtime/golang"
2424
"github.com/parca-dev/parca-agent/pkg/runtime/java"
25+
"github.com/parca-dev/parca-agent/pkg/runtime/lua"
2526
"github.com/parca-dev/parca-agent/pkg/runtime/python"
2627
"github.com/parca-dev/parca-agent/pkg/runtime/ruby"
2728
)
@@ -59,6 +60,12 @@ func Fetch(p procfs.Proc, cim *runtime.CompilerInfoManager) (runtime.UnwinderInf
5960
return nil, fmt.Errorf("failed to fetch jvm interpreter info: %w", err)
6061
}
6162
return jvmInfo, nil
63+
case runtime.UnwinderLua:
64+
luaInfo, err := lua.VMInfo(p)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to fetch lua interpreter info: %w", err)
67+
}
68+
return luaInfo, nil
6269

6370
case runtime.UnwinderNone:
6471
return nil, nil //nolint: nilnil
@@ -100,5 +107,14 @@ func determineUnwinderType(proc procfs.Proc, cim *runtime.CompilerInfoManager) (
100107
if err != nil {
101108
errs = errors.Join(errs, err)
102109
}
110+
111+
ok, err = lua.IsRuntime(proc)
112+
if ok {
113+
return runtime.UnwinderLua, nil
114+
}
115+
if err != nil {
116+
errs = errors.Join(errs, err)
117+
}
118+
103119
return runtime.UnwinderNone, errs
104120
}

‎scripts/local-run.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ trap 'kill $(jobs -p); exit 0' EXIT INT
4141
if [ -z "$DEBUG" ]; then
4242
sudo "${PARCA_AGENT}" \
4343
--node=local-test \
44-
--log-level=debug \
44+
--log-level=info \
4545
--debuginfo-upload-timeout-duration=2m \
4646
--config-path="parca-agent.yaml" \
4747
--remote-store-address=localhost:7070 \

‎test/integration/lua/lua_test.go

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2024 The Parca Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
//
14+
15+
package lua
16+
17+
import (
18+
"context"
19+
"io"
20+
"net"
21+
"net/http"
22+
"strconv"
23+
"testing"
24+
"time"
25+
26+
"github.com/prometheus/client_golang/prometheus"
27+
"github.com/stretchr/testify/require"
28+
"github.com/testcontainers/testcontainers-go"
29+
30+
"github.com/parca-dev/parca-agent/pkg/agent"
31+
"github.com/parca-dev/parca-agent/pkg/logger"
32+
"github.com/parca-dev/parca-agent/pkg/objectfile"
33+
"github.com/parca-dev/parca-agent/pkg/profiler/cpu"
34+
"github.com/parca-dev/parca-agent/test/integration"
35+
)
36+
37+
func TestLua(t *testing.T) {
38+
ok, _, err := agent.PreflightChecks(false, false, false)
39+
require.Truef(t, ok, "preflight checks failed: %v", err)
40+
if err != nil {
41+
t.Logf("preflight checks passed but with errors: %v", err)
42+
}
43+
44+
tests := []struct {
45+
program string
46+
want []string
47+
wantErr bool
48+
}{
49+
{
50+
program: "testdata/fib.conf",
51+
want: []string{"main", "Fibonacci::naive", "inner"},
52+
wantErr: false,
53+
},
54+
}
55+
for _, tt := range tests {
56+
var (
57+
program = tt.program
58+
want = tt.want
59+
name = "lua"
60+
version = "latest"
61+
)
62+
t.Run(name, func(t *testing.T) {
63+
// Start a openresty container.
64+
ctx, cancel := context.WithCancel(context.Background())
65+
t.Cleanup(cancel)
66+
67+
lua, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
68+
ContainerRequest: testcontainers.ContainerRequest{
69+
Image: "openresty/openresty:latest",
70+
ExposedPorts: []string{"80"},
71+
Files: []testcontainers.ContainerFile{
72+
{
73+
HostFilePath: "testdata/nginx.conf",
74+
ContainerFilePath: "/usr/local/openresty/nginx/conf/nginx.conf",
75+
},
76+
{
77+
HostFilePath: program,
78+
ContainerFilePath: "/etc/nginx/conf.d/default.conf",
79+
},
80+
},
81+
},
82+
Started: true,
83+
})
84+
require.NoError(t, err)
85+
86+
host, err := lua.Host(ctx)
87+
require.NoError(t, err)
88+
89+
port, err := lua.MappedPort(ctx, "80")
90+
require.NoError(t, err)
91+
92+
go func() {
93+
for {
94+
select {
95+
case <-ctx.Done():
96+
return
97+
default:
98+
}
99+
url := "http://" + net.JoinHostPort(host, port.Port())
100+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
101+
if err != nil {
102+
t.Log(err)
103+
}
104+
res, err := http.DefaultClient.Do(req)
105+
if err != nil {
106+
time.Sleep(100 * time.Millisecond)
107+
t.Log(err)
108+
continue
109+
}
110+
defer res.Body.Close()
111+
body, err := io.ReadAll(res.Body)
112+
if err != nil {
113+
t.Log(err)
114+
}
115+
showContents := false
116+
if showContents {
117+
t.Log(string(body))
118+
}
119+
}
120+
}()
121+
122+
t.Cleanup(func() {
123+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
124+
defer cancel()
125+
126+
err := lua.Terminate(ctx)
127+
if err != nil {
128+
require.ErrorIs(t, err, context.DeadlineExceeded)
129+
}
130+
})
131+
132+
state, err := lua.State(ctx)
133+
require.NoError(t, err)
134+
135+
if !state.Running {
136+
t.Logf("lua (%s) is not running", name)
137+
}
138+
139+
pid := state.Pid
140+
t.Logf("lua (%s) is running with pid %d", version, pid)
141+
142+
// Start the agent.
143+
var (
144+
profileStore = integration.NewTestAsyncProfileStore()
145+
logger = logger.NewLogger("info", logger.LogFormatLogfmt, "parca-agent-tests")
146+
reg = prometheus.NewRegistry()
147+
ofp = objectfile.NewPool(logger, reg, "", 100, 10*time.Second)
148+
)
149+
t.Cleanup(func() {
150+
profileStore.Close()
151+
ofp.Close()
152+
})
153+
154+
profiler, err := integration.NewTestProfiler(logger, reg, ofp, profileStore, t.TempDir(), &cpu.Config{
155+
ProfilingDuration: 1 * time.Second,
156+
ProfilingSamplingFrequency: uint64(27),
157+
PerfEventBufferPollInterval: 250,
158+
PerfEventBufferProcessingInterval: 100,
159+
PerfEventBufferWorkerCount: 8,
160+
MemlockRlimit: uint64(4000000),
161+
DebugProcessNames: []string{},
162+
DWARFUnwindingDisabled: false,
163+
DWARFUnwindingMixedModeEnabled: true,
164+
PythonUnwindingEnabled: false,
165+
RubyUnwindingEnabled: false,
166+
LuaUnwindingEnabled: true,
167+
BPFVerboseLoggingEnabled: true, // Enable for debugging.
168+
BPFEventsBufferSize: 8192,
169+
RateLimitUnwindInfo: 50,
170+
RateLimitProcessMappings: 50,
171+
RateLimitRefreshProcessInfo: 50,
172+
},
173+
)
174+
require.NoError(t, err)
175+
176+
integration.RunAndAwaitSamples(t, profiler, profileStore, 300*time.Second, func(t *testing.T, s integration.Sample) bool {
177+
t.Helper()
178+
foundPid, err := strconv.Atoi(string(s.Labels["ppid"]))
179+
if err != nil {
180+
t.Fatal("label pid is not a valid integer")
181+
}
182+
if foundPid != pid {
183+
return false
184+
}
185+
186+
require.Equal(t, "samples", s.Profile.SampleType[0].Type)
187+
require.Equal(t, "count", s.Profile.SampleType[0].Unit)
188+
189+
require.NotEmpty(t, s.Profile.Sample)
190+
require.NotEmpty(t, s.Profile.Location)
191+
require.NotEmpty(t, s.Profile.Mapping)
192+
193+
aggregatedStack, err := integration.AggregateStacks(s.Profile)
194+
require.NoError(t, err)
195+
196+
if integration.AnyStackContains(aggregatedStack, want) {
197+
t.Log("Got ", want)
198+
return true
199+
}
200+
return false
201+
})
202+
})
203+
}
204+
}
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# nginx.vh.default.conf -- docker-openresty
2+
#
3+
# This file is installed to:
4+
# `/etc/nginx/conf.d/default.conf`
5+
#
6+
# It tracks the `server` section of the upstream OpenResty's `nginx.conf`.
7+
#
8+
# This config (and any other configs in `etc/nginx/conf.d/`) is loaded by
9+
# default by the `include` directive in `/usr/local/openresty/nginx/conf/nginx.conf`.
10+
#
11+
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
12+
#
13+
14+
15+
server {
16+
listen 80;
17+
# server_name localhost;
18+
19+
#charset koi8-r;
20+
#access_log /var/log/nginx/host.access.log main;
21+
#error_log /var/log/nginx/error_log;
22+
23+
location / {
24+
default_type text/plain;
25+
content_by_lua_block {
26+
local out = {
27+
-- Do something with each line:
28+
write = function(t, ...) ngx.print(...) end,
29+
close = function(t) end,
30+
flush = function(t) end,
31+
}
32+
local bc = require("jit.bc")
33+
-- local function foo()
34+
Fibonacci = {}
35+
local dump=true
36+
function Fibonacci.naive(n)
37+
local function inner(m)
38+
if m < 2 then
39+
if dump then
40+
ngx.say(debug.traceback())
41+
bc.dump(debug.getinfo(1,"f").func, out)
42+
dump = false
43+
end
44+
return m
45+
end
46+
return inner(m-1) + inner(m-2)
47+
end
48+
return inner(n)
49+
end
50+
--return Fibonacci.naive(20)
51+
--end
52+
ngx.say("Fib(" .. tostring(20) .. ") = " .. tostring(Fibonacci.naive(20)))
53+
54+
--debug.getinfo(1,"f")
55+
--bc.dump(debug.getinfo(1,"f").func, out)
56+
}
57+
}
58+
59+
#error_page 404 /404.html;
60+
61+
# redirect server error pages to the static page /50x.html
62+
#
63+
error_page 500 502 503 504 /50x.html;
64+
location = /50x.html {
65+
root /usr/local/openresty/nginx/html;
66+
}
67+
68+
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
69+
#
70+
#location ~ \.php$ {
71+
# proxy_pass http://127.0.0.1;
72+
#}
73+
74+
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
75+
#
76+
#location ~ \.php$ {
77+
# root /usr/local/openresty/nginx/html;
78+
# fastcgi_pass 127.0.0.1:9000;
79+
# fastcgi_index index.php;
80+
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
81+
# include fastcgi_params;
82+
#}
83+
84+
# deny access to .htaccess files, if Apache's document root
85+
# concurs with nginx's one
86+
#
87+
#location ~ /\.ht {
88+
# deny all;
89+
#}
90+
}

0 commit comments

Comments
 (0)
Please sign in to comment.