Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Python 3.12 Support #2553

Merged
merged 4 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ VMLINUX := vmlinux.h
BPF_ROOT := bpf
BPF_SRC := $(BPF_ROOT)/unwinders/native.bpf.c
OUT_BPF_DIR := pkg/profiler/cpu/bpf/programs/objects/$(ARCH)
# TODO(kakkoyun): DRY.
OUT_BPF := $(OUT_BPF_DIR)/native.bpf.o
OUT_RBPERF := $(OUT_BPF_DIR)/rbperf.bpf.o
OUT_PYPERF := $(OUT_BPF_DIR)/pyperf.bpf.o
Expand Down Expand Up @@ -163,7 +162,6 @@ ifndef DOCKER
$(OUT_BPF): $(BPF_SRC) libbpf | $(OUT_DIR)
mkdir -p $(OUT_BPF_DIR) $(OUT_BPF_CONTAINED_DIR)
$(MAKE) -C bpf build
# TODO(kakkoyun): DRY.
cp bpf/out/$(ARCH)/native.bpf.o $(OUT_BPF)
cp bpf/out/$(ARCH)/rbperf.bpf.o $(OUT_RBPERF)
cp bpf/out/$(ARCH)/pyperf.bpf.o $(OUT_PYPERF)
Expand Down
2 changes: 0 additions & 2 deletions bpf/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ OUT_DIR ?= ../dist
OUT_BPF_BASE_DIR := out
OUT_BPF_DIR := $(OUT_BPF_BASE_DIR)/$(ARCH)
OUT_BPF := $(OUT_BPF_DIR)/native.bpf.o
# TODO(kakkoyun): DRY.
OUT_RBPERF := $(OUT_BPF_DIR)/rbperf.bpf.o
OUT_PYPERF := $(OUT_BPF_DIR)/pyperf.bpf.o
OUT_PID_NAMESPACE_DETECTOR := $(OUT_BPF_DIR)/pid_namespace.bpf.o
Expand All @@ -24,7 +23,6 @@ BPF_BUNDLE := $(OUT_DIR)/parca-agent.bpf.tar.gz
LIBBPF_HEADERS := $(OUT_DIR)/libbpf/$(ARCH)/usr/include

VMLINUX_INCLUDE_PATH := $(SHORT_ARCH)
# TODO(kakkoyun): DRY.
BPF_SRC := unwinders/native.bpf.c
RBPERF_SRC := unwinders/rbperf.bpf.c
PYPERF_SRC := unwinders/pyperf.bpf.c
Expand Down
149 changes: 116 additions & 33 deletions bpf/unwinders/pyperf.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ struct {
__type(value, PythonVersionOffsets);
} version_specific_offsets SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 12); // arbitrary
__type(key, u32);
__type(value, LibcOffsets);
} musl_offsets SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 12); // arbitrary
__type(key, u32);
__type(value, LibcOffsets);
} glibc_offsets SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
Expand Down Expand Up @@ -80,7 +94,64 @@ struct {
} \
})

static __always_inline long unsigned int read_tls_base(struct task_struct *task) {
// tls_read reads from the TLS associated with the provided key depending on the libc implementation.
static inline __attribute__((__always_inline__)) int tls_read(void *tls_base, InterpreterInfo *interpreter_info, void **out) {
LibcOffsets *libc_offsets;
void *tls_addr = NULL;
int key = interpreter_info->tls_key;
switch (interpreter_info->libc_implementation) {
case LIBC_IMPLEMENTATION_GLIBC:
// Read the offset from the corresponding map.
libc_offsets = bpf_map_lookup_elem(&glibc_offsets, &interpreter_info->libc_offset_index);
if (libc_offsets == NULL) {
LOG("[error] libc_offsets for glibc is NULL");
return -1;
}
#if __TARGET_ARCH_x86

tls_addr = tls_base + libc_offsets->pthread_block + (key * libc_offsets->pthread_key_data_size) + libc_offsets->pthread_key_data;
#elif __TARGET_ARCH_arm64
tls_addr =
tls_base - libc_offsets->pthread_size + libc_offsets->pthread_block + (key * libc_offsets->pthread_key_data_size) + libc_offsets->pthread_key_data;
#else
#error "Unsupported platform"
#endif
break;
case LIBC_IMPLEMENTATION_MUSL:
// Read the offset from the corresponding map.
libc_offsets = bpf_map_lookup_elem(&musl_offsets, &interpreter_info->libc_offset_index);
if (libc_offsets == NULL) {
LOG("[error] libc_offsets for musl is NULL");
return -1;
}
#if __TARGET_ARCH_x86
if (bpf_probe_read_user(&tls_addr, sizeof(tls_addr), tls_base + libc_offsets->pthread_block)) {
return -1;
}
tls_addr = tls_addr + key * libc_offsets->pthread_key_data_size;
#elif __TARGET_ARCH_arm64
if (bpf_probe_read_user(&tls_addr, sizeof(tls_addr), tls_base - libc_offsets->pthread_size + libc_offsets->pthread_block)) {
return -1;
}
tls_addr = key * libc_offsets->pthread_key_data_size;
#else
#error "Unsupported platform"
#endif
break;
default:
LOG("[error] unknown libc_implementation %d", interpreter_info->libc_implementation);
return -1;
}

LOG("tls_read key %d from address 0x%lx", key, (unsigned long)tls_addr);
if (bpf_probe_read(out, sizeof(*out), tls_addr)) {
LOG("failed to read 0x%lx from TLS", (unsigned long)tls_addr);
return -1;
}
return 0;
}

static inline __attribute__((__always_inline__)) long unsigned int read_tls_base(struct task_struct *task) {
long unsigned int tls_base;
// This changes depending on arch and kernel version.
// task->thread.fs, task->thread.uw.tp_value, etc.
Expand Down Expand Up @@ -108,7 +179,6 @@ int unwind_python_stack(struct bpf_perf_event_data *ctx) {
return 1;
}

// TODO(kakkoyun) : DRY.
u64 pid_tgid = bpf_get_current_pid_tgid();
pid_t pid = pid_tgid >> 32;
pid_t tid = pid_tgid;
Expand All @@ -122,11 +192,6 @@ int unwind_python_stack(struct bpf_perf_event_data *ctx) {
return 0;
}

if (interpreter_info->thread_state_addr == 0) {
LOG("[error] interpreter_info.thread_state_addr was NULL");
return 0;
}

LOG("[start]");
LOG("[event] pid=%d tid=%d", pid, tid);

Expand Down Expand Up @@ -157,50 +222,68 @@ int unwind_python_stack(struct bpf_perf_event_data *ctx) {

// Fetch thread state.

// GDB: ((PyThreadState *)_PyRuntime.gilstate.tstate_current)
LOG("interpreter_info->thread_state_addr 0x%llx", interpreter_info->thread_state_addr);
int err = bpf_probe_read_user(&state->thread_state, sizeof(state->thread_state), (void *)(long)interpreter_info->thread_state_addr);
if (err != 0) {
LOG("[error] failed to read interpreter_info->thread_state_addr with %d", err);
goto submit_without_unwinding;
}
if (state->thread_state == 0) {
LOG("[error] thread_state was NULL");
goto submit_without_unwinding;
if (interpreter_info->thread_state_addr != 0) {
LOG("interpreter_info->thread_state_addr 0x%llx", interpreter_info->thread_state_addr);
int err = bpf_probe_read_user(&state->thread_state, sizeof(state->thread_state), (void *)(long)interpreter_info->thread_state_addr);
if (err != 0) {
LOG("[error] failed to read interpreter_info->thread_state_addr with %d", err);
goto submit_without_unwinding;
}
LOG("thread_state 0x%llx", state->thread_state);
}
LOG("thread_state 0x%llx", state->thread_state);

struct task_struct *task = (struct task_struct *)bpf_get_current_task();
long unsigned int tls_base = read_tls_base(task);
LOG("tls_base 0x%llx", (void *)tls_base);
if (interpreter_info->use_tls) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
long unsigned int tls_base = read_tls_base(task);
LOG("tls_base 0x%llx", (void *)tls_base);

// TODO(kakkoyun): Read TLS key in here instead of user-space.
// int key;
// if (bpf_probe_read(&key, sizeof(key), interpreter_info->tls_key_addr)) {
// LOG("[error] failed to read TLS key from 0x%lx", (unsigned long)interpreter_info->tls_key_addr);
// goto submit_without_unwinding;
// }
if (tls_read((void *)tls_base, interpreter_info, &state->thread_state)) {
LOG("[error] failed to read thread state from TLS 0x%lx", (unsigned long)interpreter_info->tls_key);
goto submit_without_unwinding;
}

if (state->thread_state == 0) {
LOG("[error] thread_state was NULL");
goto submit_without_unwinding;
}
LOG("thread_state 0x%llx", state->thread_state);
}

GET_OFFSETS();

// Fetch the thread id.

LOG("offsets->py_thread_state.thread_id %d", offsets->py_thread_state.thread_id);
pthread_t pthread_id;
bpf_probe_read_user(&pthread_id, sizeof(pthread_id), state->thread_state + offsets->py_thread_state.thread_id);
if (bpf_probe_read_user(&pthread_id, sizeof(pthread_id), state->thread_state + offsets->py_thread_state.thread_id)) {
LOG("[error] failed to read thread_state->thread_id");
goto submit_without_unwinding;
}

LOG("pthread_id %lu", pthread_id);
// 0x10 = offsetof(tcbhead_t, self) for glibc x86.
// pthread_t current_pthread_id;
// bpf_probe_read_user(&current_pthread_id, sizeof(current_pthread_id), (void *)(tls_base + 0x10));
// LOG("current_pthread_id %lu", current_pthread_id);
// if (pthread_id != current_pthread_id) {
// LOG("[error] pthread_id %lu != current_pthread_id %lu", pthread_id, current_pthread_id);
// goto submit_without_unwinding;
// }
// state->current_pthread = current_pthread_id;
state->current_pthread = pthread_id;

// Get pointer to top frame from PyThreadState.

if (offsets->py_thread_state.frame > -1) {
LOG("offsets->py_thread_state.frame %d", offsets->py_thread_state.frame);
bpf_probe_read_user(&state->frame_ptr, sizeof(void *), state->thread_state + offsets->py_thread_state.frame);
if (bpf_probe_read_user(&state->frame_ptr, sizeof(void *), state->thread_state + offsets->py_thread_state.frame)) {
LOG("[error] failed to read thread_state->frame");
goto submit_without_unwinding;
}
} else {
LOG("offsets->py_thread_state.cframe %d", offsets->py_thread_state.cframe);
void *cframe;
bpf_probe_read_user(&cframe, sizeof(cframe), (void *)(state->thread_state + offsets->py_thread_state.cframe));
if (bpf_probe_read_user(&cframe, sizeof(cframe), (void *)(state->thread_state + offsets->py_thread_state.cframe))) {
LOG("[error] failed to read thread_state->cframe");
goto submit_without_unwinding;
}
if (cframe == 0) {
LOG("[error] cframe was NULL");
goto submit_without_unwinding;
Expand Down
18 changes: 18 additions & 0 deletions bpf/unwinders/pyperf.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@

#define PYPERF_STACK_WALKING_PROGRAM_IDX 0

enum libc_implementation {
LIBC_IMPLEMENTATION_GLIBC = 0,
LIBC_IMPLEMENTATION_MUSL = 1,
};

typedef struct {
// u64 start_time;
// u64 interpreter_addr;
u64 thread_state_addr;
u64 tls_key;
u32 py_version_offset_index;
u32 libc_offset_index;
enum libc_implementation libc_implementation;

_Bool use_tls;
// TODO(kakkoyun): bool use_runtime_debug_offsets;
} InterpreterInfo;

enum python_stack_status {
Expand Down Expand Up @@ -127,3 +138,10 @@ typedef struct {
PyTupleObject py_tuple_object;
PyTypeObject py_type_object;
} PythonVersionOffsets;

typedef struct {
s64 pthread_size;
s64 pthread_block;
s64 pthread_key_data;
s64 pthread_key_data_size;
} LibcOffsets;
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ require (
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1
github.com/klauspost/compress v1.17.6
github.com/klauspost/compress v1.17.7
github.com/minio/highwayhash v1.0.2
github.com/oklog/run v1.1.0
github.com/opencontainers/runtime-spec v1.2.0
github.com/parca-dev/parca v0.20.0
github.com/parca-dev/runtime-data v0.0.0-20240221144835-838a856ad496
github.com/parca-dev/runtime-data v0.0.0-20240225202746-241008d5f5c3
github.com/planetscale/vtprotobuf v0.6.0
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/common v0.47.0
Expand Down Expand Up @@ -201,11 +201,11 @@ require (
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel/metric v1.23.1 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect
Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
Expand Down Expand Up @@ -495,8 +495,8 @@ github.com/oracle/oci-go-sdk/v65 v65.53.0 h1:/h+rzaRw7W1eSTeDLhSMTRnyXg1oj5NTPeB
github.com/oracle/oci-go-sdk/v65 v65.53.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/parca-dev/parca v0.20.0 h1:G/TdZLbZEArZzd86rI2RqMpUtz0verlvO3+NDP/d0m0=
github.com/parca-dev/parca v0.20.0/go.mod h1:dKRKjo1MK83iEUygyINUYTAriHICGYejD4EWm5MGT+0=
github.com/parca-dev/runtime-data v0.0.0-20240221144835-838a856ad496 h1:sTUSBmbMriumcql8S8UwHtfpzYO2GfaivNEs9eY0Ty4=
github.com/parca-dev/runtime-data v0.0.0-20240221144835-838a856ad496/go.mod h1:NRpa9nozMNUeyw6p6KeuSx+leWBMoJJE7+mN7XUNdpA=
github.com/parca-dev/runtime-data v0.0.0-20240225202746-241008d5f5c3 h1:hlxBPG8wcI0fd2RLJHKcPIFO36k+9mEMpYb3e8NFbSg=
github.com/parca-dev/runtime-data v0.0.0-20240225202746-241008d5f5c3/go.mod h1:zSTEFju13hAb35sGVtDwWXXHhRPoVQjDnFEpltj6du4=
github.com/parquet-go/parquet-go v0.19.0 h1:xtHOBIE0/8CRhmf06V1GJ7q3qARY2/kXiSweFlscwUQ=
github.com/parquet-go/parquet-go v0.19.0/go.mod h1:6pu/Ca02WRyWyF6jbY1KceESGBZMsRMSijjLbajXaG8=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
Expand Down Expand Up @@ -683,8 +683,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -729,8 +729,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -785,8 +785,8 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
Expand Down
Loading
Loading