diff --git a/README.md b/README.md index 02fc938..e75d413 100755 --- a/README.md +++ b/README.md @@ -1,381 +1,245 @@ # ZVFS -ZVFS 是一个基于 SPDK Blobstore 的用户态文件系统原型,目标是在不改业务代码的前提下,将常见 POSIX 文件 I/O 重定向到用户态高性能存储路径。 -核心思想是复用 Linux 文件管理机制(命名空间/目录/元数据),把文件数据平面放到 ZVFS。 +> 透明用户态 POSIX 文件系统,基于 SPDK Blobstore。 -- Hook 方式:`LD_PRELOAD` -- 挂载前缀:`/zvfs` -- 架构:多进程 Client + 独立 Daemon + SPDK -- 语义:同步阻塞(请求-响应) +ZVFS 是一个 **透明用户态文件系统原型**,通过 `LD_PRELOAD` 劫持 POSIX I/O, +将应用程序的文件数据路径从 Linux 内核 I/O 栈重定向到 **SPDK 用户态 NVMe 存储路径**。 + +目标是在 **零业务代码修改** 的情况下,为数据库与向量检索系统提供更低延迟的存储访问。 + +目前已在 **PostgreSQL + pgvector** 场景完成功能验证。 --- -## 1. 项目定位 +# 设计思路 -这个项目重点不只是“把 I/O 跑起来”,而是把以下工程问题串起来: +大多数用户态文件系统(如 FUSE)需要修改应用或挂载文件系统。用户态文件系统如果要通过VFS,需要多一到两次额外的用户态/内核态切换。ZVFS 的目标是对应用完全透明:应用按正常方式调用 POSIX API,底层存储路径被悄悄替换掉。 +核心决策是控制面与数据面分离: -1. 在多线程/多进程应用(RocksDB / PostgreSQL)里做透明接管。 -2. 保留 POSIX 语义(open/close/dup/fork/append/sync 等)。 -3. 把 SPDK 资源集中在 daemon 管理,避免每进程重复初始化。 -4. 在同步阻塞语义下,把协议、并发、错误处理做完整。 +控制面复用 Linux VFS:目录树、权限、inode 生命周期全部由 Linux 管理,文件到 blob 的映射通过 xattr(user.zvfs.blob_id)持久化,无需额外的映射数据库。 +数据面走 SPDK:read/write 等数据路径绕过内核,经 IPC 送到 ZVFS daemon,再通过 SPDK Blobstore 直接访问 NVMe。 + +``` +Application (PostgreSQL / RocksDB) + │ POSIX API + ▼ + LD_PRELOAD Hook Layer + │ Unix Domain Socket + ▼ + ZVFS Daemon + ┌────┴────┐ + │ │ +Metadata IO Workers + Thread (SPDK pollers) + │ │ + └────┬────┘ + ▼ + SPDK Blobstore + │ + NVMe SSD +``` + +SPDK 需要使用轮询模式,最好能独占CPU core,且metadata最好由同一个 spdk thread 管理,不适合嵌入任意应用进程。因此 daemon 统一持有所有 SPDK 资源,多个客户端进程共享同一个 daemon,通过 Unix Domain Socket 通信。 --- -## 2. 架构设计 +# 🧠 系统架构 ![](zvfs架构图.excalidraw.svg) -```text -App (PostgreSQL / RocksDB / db_bench / pgbench) - -> LD_PRELOAD libzvfs.so - -> Hook Client (POSIX 拦截 + 本地状态) - -> Unix Domain Socket IPC (sync/blocking) - -> zvfs_daemon - -> 协议反序列化 + 分发 - -> metadata thread + io threads - -> SPDK Blobstore / bdev -``` +架构设计关键点: -### 2.1 透传策略 +- **同步阻塞语义** +- **零侵入接管应用 I/O** + - 使用 `LD_PRELOAD` 拦截 POSIX API + - 不需要修改应用代码 +- **控制面复用 Linux** + - ZVFS 不重新实现目录树,而是复用 Linux VFS。目录 / 权限 / inode 生命周期由 Linux VFS 管理。 + - 文件与 blob 的映射通过:`xattr: user.zvfs.blob_id` +- **SPDK 资源集中管理** + - 文件内容存储在 SPDK Blobstore。直接访问 NVMe。 + - SPDK 对 metadata 操作有 **单线程要求**,因此 daemon 设计为: + - metadata 操作:create / resize / delete + - data IO:read / write -**控制面复用 Linux,数据面走 ZVFS**。 - -- 控制面(Linux 负责) - - 目录/命名空间管理。 - - 文件节点生命周期与权限语义(create/open/close/stat/rename/unlink 等)。 - - 这些操作在 `/zvfs` 下也会真实执行系统调用,ZVFS 不重复实现目录树管理。 - -- 数据面(ZVFS 负责) - - 文件内容读写由 blob 承载。 - - `read/write` 的真实数据路径不走 Linux 文件数据面,而走 ZVFS IPC + SPDK。 - -- 关键绑定方式 - - `create`:真实创建 Linux 文件 + 在 ZVFS 创建 blob + 把 `blob_id` 写入文件 xattr。 - - `open`:真实 `open` Linux 文件 + 读取 xattr 获取 `blob_id` + 在 ZVFS 打开 blob。 - - `write`:写入 blob 成功后,使用 `ftruncate` 同步 Linux 视角 `st_size`。 - -- 工程收益 - - 直接减少约 50% 的实现工作量。 - - 兼容性更好,数据库可直接复用现有文件组织方式。 - -### 2.2 分层职责 - -- Client(`src/hook` + `src/spdk_engine/io_engine.c`) - - 判断是否 `/zvfs` 路径。 - - 拦截 POSIX API 并发起同步 IPC。 - - 维护最小本地状态(`fd_table/path_cache/inode_table`)。 - -- Daemon(`src/daemon`) - - 独占 SPDK 环境与线程。 - - 统一执行 blob create/open/read/write/resize/sync/delete。 - - 统一管理 handle/ref_count。 - -- 协议层(`src/proto/ipc_proto.*`) - - 统一头 + per-op body。 - - Request Header:`opcode + payload_len` - - Response Header:`opcode + status + payload_len` - -### 2.3 为什么是同步阻塞 IPC - -- 业务侧兼容成本低,最容易对齐 POSIX 语义。 -- 调试路径更直接(一个请求对应一个响应)。 -- 先解决正确性和语义完整,再考虑异步化。 +- **POSIX 语义兼容** + - 用户态文件系统需要正确模拟 Linux FD 语义:`dup dup2 dup3 fork close_range` + - 保证多个 fd 指向同一文件句柄时语义一致。 --- -## 3. 功能覆盖(当前) - -### 3.1 已接管的核心接口 - -- 控制面协同:`open/openat/creat/rename/unlink/...`(真实 syscall + ZVFS 元数据协同) -- 数据面接管:`read/write/pread/pwrite/readv/writev/pwritev` -- 元数据:`fstat/lseek/ftruncate/fallocate` -- 同步:`fsync/fdatasync/sync_file_range` -- FD 语义:`dup/dup2/dup3/fork/close_range` - -### 3.2 语义要点 - -- `write` 默认使用 `AUTO_GROW`。 -- 非 `AUTO_GROW` 写越界返回 `ENOSPC`。 -- `O_APPEND` 语义由 inode `logical_size` 保证。 -- `write` 成功后会同步更新 Linux 文件大小(`ftruncate`),保持 `stat` 视角一致。 -- `mmap` 对 zvfs fd 当前返回 `ENOTSUP`(非 zvfs fd 透传)。 - -### 3.3 映射关系 - -- 文件数据在 SPDK blob 中。 -- 文件到 blob 的映射通过 xattr:`user.zvfs.blob_id`。 - ---- - -## 4. 构建与运行 - -### 4.1 构建 +# 📦 构建 ```bash -cd zvfs -git submodule update --init --recursive --progress +git clone ... +git submodule update --init --recursive cd spdk ./scripts/pkgdep.sh ./configure --with-shared -make -j"$(nproc)" +make -j cd .. -make -j"$(nproc)" -mkdir -p tests/bin -make test -j"$(nproc)" +make -j ``` +--- -产物: +# ▶️ 运行 -- `src/libzvfs.so` -- `src/daemon/zvfs_daemon` -- `tests/bin/*` - -### 4.2 启动 daemon - -```bash -cd zvfs +启动 daemon: +``` ./src/daemon/zvfs_daemon ``` -可选环境变量: - -- `SPDK_BDEV_NAME` -- `SPDK_JSON_CONFIG` -- `ZVFS_SOCKET_PATH` / `ZVFS_IPC_SOCKET_PATH` - -### 4.3 快速验证 - -```bash -mkdir -p /zvfs -LD_PRELOAD=./src/libzvfs.so ZVFS_TEST_ROOT=/zvfs ./tests/bin/hook_api_test -./tests/bin/ipc_zvfs_test +运行测试: ``` - ---- - -## 5. 性能测试 - -### 5.1 测试目标 - -- 目标场景:低队列深度下阻塞 I/O 性能。 -- 对比对象:`spdk_nvme_perf` 与内核路径(`O_DIRECT`)。 - -### 5.2 工具与脚本 - -- RocksDB:`scripts/run_db_bench_zvfs.sh` -- PostgreSQL:`codex/run_pgbench_no_mmap.sh` - -建议: - -- PostgreSQL 测试时关闭 mmap 路径(shared memory 改为 sysv,避免 mmap 干扰)。 - -### 5.3 历史结果 - -- QD=1 下可达到 `spdk_nvme_perf` 的约 `90%~95%`。 -- 相对同机 `O_DIRECT`,顺序写吞吐可有约 `2.2x~2.3x` 提升。 -- 非对齐写因 RMW 开销,吞吐明显下降。 - -### 5.4 fio -```shell -root@ubuntu20-129:/home/lian/share/zvfs# LD_PRELOAD=/home/lian/share/zvfs/src/libzvfs.so fio ./fio_script/zvfs.fio -test: (g=0): rw=randwrite, bs=(R) 16.0KiB-16.0KiB, (W) 16.0KiB-16.0KiB, (T) 16.0KiB-16.0KiB, ioengine=psync, iodepth=64 -fio-3.16 -Starting 1 thread -test: Laying out IO file (1 file / 0MiB) -Jobs: 1 (f=1): [w(1)][100.0%][w=13.4MiB/s][w=857 IOPS][eta 00m:00s] -test: (groupid=0, jobs=1): err= 0: pid=16519: Sat Mar 14 14:11:27 2026 - Description : ["variable bs"] - write: IOPS=829, BW=12.0MiB/s (13.6MB/s)(130MiB/10001msec); 0 zone resets - clat (usec): min=778, max=4000, avg=1199.89, stdev=377.74 - lat (usec): min=779, max=4001, avg=1200.38, stdev=377.78 - clat percentiles (usec): - | 1.00th=[ 848], 5.00th=[ 898], 10.00th=[ 922], 20.00th=[ 955], - | 30.00th=[ 979], 40.00th=[ 1004], 50.00th=[ 1029], 60.00th=[ 1074], - | 70.00th=[ 1221], 80.00th=[ 1500], 90.00th=[ 1614], 95.00th=[ 1975], - | 99.00th=[ 2606], 99.50th=[ 2966], 99.90th=[ 3359], 99.95th=[ 3425], - | 99.99th=[ 4015] - bw ( KiB/s): min=10048, max=15520, per=99.91%, avg=13258.32, stdev=1465.96, samples=19 - iops : min= 628, max= 970, avg=828.63, stdev=91.62, samples=19 - lat (usec) : 1000=38.46% - lat (msec) : 2=56.79%, 4=4.74%, 10=0.01% - cpu : usr=5.27%, sys=0.00%, ctx=8499, majf=0, minf=7 - IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0% - submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% - complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% - issued rwts: total=0,8295,0,0 short=0,0,0,0 dropped=0,0,0,0 - latency : target=0, window=0, percentile=100.00%, depth=64 - -Run status group 0 (all jobs): - WRITE: bw=12.0MiB/s (13.6MB/s), 12.0MiB/s-12.0MiB/s (13.6MB/s-13.6MB/s), io=130MiB (136MB), run=10001-10001msec - -Disk stats (read/write): - sda: ios=0/118, merge=0/104, ticks=0/66, in_queue=67, util=0.24% -``` - -### 5.5 pgbench -```shell -root@ubuntu20-129:/home/lian/share/zvfs# ./scripts/run_pgbench_no_mmap.sh -当前配置: - host=127.0.0.1 port=5432 db=benchdb - scale=1 clients=1 threads=1 time=15s preload=1 - init_jobs=1 init_steps=dtg skip_init=0 - -[1/2] pgbench 初始化(-i) -dropping old tables... -NOTICE: table "pgbench_accounts" does not exist, skipping -NOTICE: table "pgbench_branches" does not exist, skipping -NOTICE: table "pgbench_history" does not exist, skipping -NOTICE: table "pgbench_tellers" does not exist, skipping -creating tables... -generating data... -100000 of 100000 tuples (100%) done (elapsed 15.06 s, remaining 0.00 s) -done. -[2/2] pgbench 压测(-T) -starting vacuum...end. -transaction type: -scaling factor: 1 -query mode: simple -number of clients: 1 -number of threads: 1 -duration: 15 s -number of transactions actually processed: 564 -latency average = 26.614 ms -tps = 37.573586 (including connections establishing) -tps = 38.176262 (excluding connections establishing) -``` - - -```shell -root@ubuntu20-129:/home/lian/share/zvfs# ./scripts/run_pgbench.sh -当前配置: - host=127.0.0.1 port=5432 db=postgres - scale=1 clients=1 threads=1 time=15s preload=0 - init_jobs=1 init_steps=dtg skip_init=0 - -[1/2] pgbench 初始化(-i) -dropping old tables... -NOTICE: table "pgbench_accounts" does not exist, skipping -NOTICE: table "pgbench_branches" does not exist, skipping -NOTICE: table "pgbench_history" does not exist, skipping -NOTICE: table "pgbench_tellers" does not exist, skipping -creating tables... -generating data... -100000 of 100000 tuples (100%) done (elapsed 1.08 s, remaining 0.00 s) -done. -[2/2] pgbench 压测(-T) -starting vacuum...end. -transaction type: -scaling factor: 1 -query mode: simple -number of clients: 1 -number of threads: 1 -duration: 15 s -number of transactions actually processed: 586 -latency average = 25.602 ms -tps = 39.059387 (including connections establishing) -tps = 39.102273 (excluding connections establishing) +LD_PRELOAD=./src/libzvfs.so ./tests/bin/hook_api_test ``` --- -## 6. 关键工程难点与踩坑复盘(重点) - -### SPDK 元数据回调线程模型 - -问题:把 metadata 操作随意派发到任意线程,容易卡住或回调不回来。metadata的回调默认派发给初始化blobstore的线程。 - -blobstore metadata 操作与创建线程/通道绑定。 - -需要明确 metadata thread 和 io thread 分工。 - -### resize 导致程序卡死 -- `resize/delete/unload` 内部会走 `spdk_for_each_channel()` barrier。大概是让其他的spdk线程同步resize之后的状态才能返回。所以如果要做要尽可能少做resize。 - -如果其他spdk线程持有iochannel,并且没有持续poll,就会导致卡死。 -- 保证持有 channel 的线程持续 poll。 -- 线程退出时严格释放 channel,避免 barrier 永久等待。 - -### PostgreSQL Tablespace 无法命中 Hook - -现象:建表空间后文件操作路径是 `pg_tblspc/...`,daemon 无请求日志。 - -根因: - -- PostgreSQL 通过符号链接访问 tablespace。 -- 仅按字符串前缀 `/zvfs` 判断会漏判。 - -修复: - -- 路径判定增加 `realpath()` 后再判断。 -- `O_CREAT` 且文件尚不存在时,使用 `realpath(parent)+basename` 判定。 - -### PostgreSQL 报 `Permission denied`(跨用户连接 daemon) - -现象:`CREATE DATABASE ... TABLESPACE ...` 报权限错误。 - -根因: - -- daemon 由 root 启动,UDS 文件权限受 umask 影响。 -- postgres 用户无法 `connect(/tmp/zvfs.sock)`。 - -修复: - -- daemon `bind` 后显式 `chmod(socket, 0666)`。 - -### PostgreSQL 报 `Message too long` - -现象:部分 SQL(尤其 `CREATE DATABASE` 路径)失败,错误为 `Message too long`。 - -根因: - -- 不是 daemon 解析失败,而是 client 序列化请求时超出 `ZVFS_IPC_BUF_SIZE`。 -- 当前 hook 会把 `writev` 聚合成一次大写请求,容易触发上限。 - -当前处理: - -- 将 `ZVFS_IPC_BUF_SIZE` 提高到 `16MB`(`src/common/config.h`)。 - -后续优化方向: - -- 在 client `blob_write_ex` 做透明分片发送(保持同步阻塞语义)。 - -### dup/dup2/fork 语义一致性 - -问题:多个 fd 指向同一 open file description 时,如何保证 handle 引用计数一致。 - -方案: - -- 协议增加 `ADD_REF` / `ADD_REF_BATCH`。 -- 在 hook 中对 `dup/dup2/dup3/fork` 明确执行引用增加。 -- `close_range` 增加边界保护(避免 `UINT_MAX` 场景死循环)。 - -### pg \c 调用链条 -1. psql \c ... -2. fork backend -3. InitPostgres -> ValidatePgVersion ... - -4. libc fopen -5. libc 内部 -> __IO_file_fopen -> _IO_file_open -> __open64 -6. kernel openat -7. fscanf -8. libc __isoc99_fscanf -> \_IO\_* -> __read -9. kernel read - -glic 内部调用走的不是 动态符号定位,可能是一些隐藏别名。可能会绕过hook。需要拦截非常多变体 +# 🔬 已实现功能 +打开/关闭/删除 +``` +open open64 openat openat64 fopen fopen64 +creat creat64 +fclose close close_range +dup dup2 dup3 fork +unlink unlinkat remove rename renameat +``` +读写层 +``` +read pread pread64 readv preadv preadv64 preadv2 +write pwrite pwrite64 writev pwritev pwritev64 pwritev2 +fread_unlocked fread fscanf +``` +偏移/空间管理层 +``` +lseek lseek64 +truncate truncate64 ftruncate ftruncate64 fallocate posix_fallocate +``` +元数据层 +``` +stat stat64 fstat fstat64 lstat lstat64 fstatat fstatat64 statx +``` +同步/控制层 +``` +fsync fdatasync sync_file_range +fcntl fcntl64 ioctl +``` --- -## 7. 当前限制与下一步 +# 🚀 性能 -### 7.1 当前限制 +测试环境:VMware 虚拟机 + 模拟 NVMe,单线程阻塞 I/O。 -- 单请求仍受 `ZVFS_IPC_BUF_SIZE` 约束。 -- `mmap` 暂不支持 zvfs fd。 -- `ADD_REF_BATCH` 当前优先功能,不保证原子性。 +> 注:VMware 模拟 NVMe 无法体现 SPDK 轮询模式对中断驱动 I/O 的延迟优势, +> 以下数据用于评估 hook 层与 IPC 的额外开销,不代表真实硬件上的性能对比。 -### 7.2 下一步计划 +### 顺序写吞吐 -1. 实现 `WRITE` 客户端透明分片,彻底消除单包上限问题。 -2. 持续完善 PostgreSQL 场景(tablespace + pgbench + crash/restart)。 -3. 补齐更系统的性能复测(固定硬件、固定参数、全量报告)。 +| Block Size | spdk_nvme_perf | ZVFS | +|---|---|---| +| 4K | 100 MiB/s | 94 MiB/s | +| 128K | 1843 MiB/s | 1662 MiB/s | + +ZVFS 达到 **SPDK 原生性能约 90%**。 + +--- + +### fio 随机写(16K,psync) + +| | kernel (psync) | ZVFS | +|---|---|---| +| IOPS | 1855 | 1353 | +| 吞吐 | 28.0 MiB/s | 21.2 MiB/s | +| avg clat | 492 µs | 692 µs | +| sys% | 28.6% | 8.4% | + +> 当前 ZVFS 在该单线程 `psync` 随机写场景下达到 kernel `psync` 的约 73% IOPS。daemon 内部 `SPDK + reply_q` 已收敛到较稳定范围,剩余主要开销集中在 `client -> daemon` 请求进入阶段。 + +--- + +### WRITE 请求端到端延迟分解(单位 µs) +基于 12 条 `WRITE` trace 样本统计,下面按调用栈层级展开平均耗时。由于四舍五入,父子项相加会有 `±1 µs` 误差。 + +```text +total 748 +├─ c2s 317 +│ ├─ send 39 +│ └─ server_rx_wait 278 +├─ server 336 +│ ├─ rx_dispatch 12 +│ ├─ dispatch_spdk 25 +│ ├─ spdk 194 +│ └─ reply_q 103 +│ ├─ spdk_post 11 +│ └─ cq_wait 91 +│ ├─ kick 13 +│ ├─ wake_sched 65 +│ └─ wake_to_tx 12 +└─ s2c 95 + ├─ resp_wait 83 + └─ parse 12 +``` + +当前 `WRITE` 的主要额外开销已经比较清晰:一是 `c2s / server_rx_wait`,二是 `server` 内部的 `spdk` 与 `reply_q`。在 `reply_q` 中,`wake_sched` 已明显大于 `kick` 和 `wake_to_tx`,说明回包路径的主要损耗不在 `eventfd` 写入本身,而在 reactor 被唤醒后的调度等待。 + +--- + +### pgbench(PostgreSQL TPC-B,单客户端) + +| | kernel | ZVFS | +|---|---|---| +| TPS | 39.1 | 38.2 | +| avg latency | 25.6 ms | 26.6 ms | + +端到端数据库工作负载下,IPC 开销被稀释,ZVFS 与 kernel 路径性能基本持平(~4% 差距)。 + +--- + +# ⚠️ 当前局限 +- 不支持 mmap +- 非对齐写存在 RMW 开销 +- IPC 请求大小存在上限:大 I/O 需在 hook 层分片;改用共享内存 scatter-gather 可消除此限制。 +--- + +## future work +- 支持 mmap:可通过 /dev/shm + userfaultfd 方向探索。 +- 缓解非对齐写开销、`!O_DIRECT`语义:实现 类似 pagecache 的bufferpool +- 修改IPC方式:使用更快的 Shared Memory +- 减少通信、拷贝开销:将 I/O 操作迁移至 Application 进程。MetaData操作保留在 Daemon 中。 + +--- + +# 🧩 遇到的一些问题 + +## SPDK metadata 线程模型 + +SPDK Blobstore metadata 回调必须在初始化线程执行, +需要严格区分: +- metadata thread +-io thread + +否则会导致 callback 无法返回。resize barrier 卡死 + +## spdk_for_each_channel() 在 resize / delete 中会触发 barrier, +如果某些线程未 poll 会导致系统卡死。 + +解决方式: + +保证所有 IO thread 持续 poll +thread 退出时释放 io_channel + + +## PostgreSQL tablespace hook 失效 + +PostgreSQL tablespace 通过 symbolic link 访问路径: pg_tblspc/xxx + +简单字符串前缀匹配 /zvfs 会漏判。 + +解决:realpath() 后再判断路径 + + +## write 延迟显著高于预期 + +这次 fio 延迟排查里,最初 `WRITE` 延迟明显高于预期。沿端到端路径加轻量打点后发现问题并不在 SPDK 本体,而是同时叠加了无条件 RMW、VM 中 poller 调度抖动、线程未绑核,以及后期 trace 暴露出来的 reactor 唤醒后核心切换抖动。对应处理是:整块对齐写跳过 read phase、将 reactor/md/io 线程固定到指定 CPU,并把 io 线程数和绑核目标收敛到配置项中。修复后 `dispatch_spdk` 从毫秒级降到几十微秒,`WRITE` 平均延迟也回落到约 700 µs,但剩余尾延迟仍主要表现为请求进入与回包阶段的调度等待。 diff --git a/fio_script/psync.fio b/fio_script/psync.fio index 13618e7..18fd81b 100755 --- a/fio_script/psync.fio +++ b/fio_script/psync.fio @@ -7,10 +7,10 @@ verify=0 time_based=1 runtime=10 bs=16K -size=16384 +size=524288 iodepth=64 rw=randwrite -filename=kingfs +filename=/tmp/kingfs ioengine=psync [test] diff --git a/fio_script/zvfs.fio b/fio_script/zvfs.fio index 3b3ccb6..3096d51 100644 --- a/fio_script/zvfs.fio +++ b/fio_script/zvfs.fio @@ -7,7 +7,7 @@ verify=0 time_based=1 runtime=10 bs=16K -size=16384 +size=524288 iodepth=64 rw=randwrite filename=/zvfs/fio/zvfsfio diff --git a/scripts/run_pgbench.sh b/scripts/run_pgbench.sh index 030dd9b..37821e9 100755 --- a/scripts/run_pgbench.sh +++ b/scripts/run_pgbench.sh @@ -12,7 +12,7 @@ set -euo pipefail PG_HOST="127.0.0.1" PG_PORT="5432" PG_DB="postgres" -PG_SCALE="10" +PG_SCALE="1" PG_TIME="15" PG_CLIENTS="32" PG_THREADS="8" diff --git a/scripts/run_pgbench_zvfs.sh b/scripts/run_pgbench_zvfs.sh index 1850130..9421125 100755 --- a/scripts/run_pgbench_zvfs.sh +++ b/scripts/run_pgbench_zvfs.sh @@ -12,7 +12,7 @@ set -euo pipefail PG_HOST="127.0.0.1" PG_PORT="5432" PG_DB="benchdb" -PG_SCALE="10" +PG_SCALE="1" PG_TIME="15" PG_CLIENTS="32" PG_THREADS="8" diff --git a/src/common/config.h b/src/common/config.h index edd27f8..9b091c3 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -19,4 +19,14 @@ // #define ZVFS_IPC_BUF_SIZE 4096 #define ZVFS_IPC_BUF_SIZE (16 * 1024 * 1024) +/* + * 线程布局: + * io 线程从 ZVFS_IO_CPU_START 开始连续绑定; + * reactor / md 默认放到 io 线程之后,避开 VM 中常见更嘈杂的 CPU0/1。 + */ +#define ZVFS_IO_CPU_START 2 +#define ZVFS_IO_THREAD_COUNT 3 +#define ZVFS_REACTOR_CPU (ZVFS_IO_CPU_START + ZVFS_IO_THREAD_COUNT) +#define ZVFS_MD_CPU (ZVFS_REACTOR_CPU + 1) + #endif // __ZVFS_CONFIG_H__ diff --git a/src/daemon/ipc_cq.c b/src/daemon/ipc_cq.c index 6217f70..883736e 100644 --- a/src/daemon/ipc_cq.c +++ b/src/daemon/ipc_cq.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include struct cq *g_cq; @@ -11,11 +13,19 @@ struct cq *CQ_Create(void) { struct cq *q = (struct cq*)malloc(sizeof(*q)); if (!q) return NULL; q->head = q->tail = NULL; + q->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (q->wake_fd < 0) { + free(q); + return NULL; + } pthread_mutex_init(&q->lock, NULL); return q; } void CQ_Destroy(struct cq *q) { + if (!q) { + return; + } while (q->head) { struct cq_item *tmp = q->head; q->head = tmp->next; @@ -23,6 +33,9 @@ void CQ_Destroy(struct cq *q) { free(tmp->resp); free(tmp); } + if (q->wake_fd >= 0) { + close(q->wake_fd); + } pthread_mutex_destroy(&q->lock); free(q); } @@ -41,6 +54,15 @@ void CQ_Push(struct cq *q, struct zvfs_resp *resp) { q->head = q->tail = item; } pthread_mutex_unlock(&q->lock); + + if (q->wake_fd >= 0) { + uint64_t one = 1; + ssize_t rc; + + do { + rc = write(q->wake_fd, &one, sizeof(one)); + } while (rc < 0 && errno == EINTR); + } } /* 弹出响应 */ @@ -58,4 +80,11 @@ struct zvfs_resp *CQ_Pop(struct cq *q) { struct zvfs_resp *resp = item->resp; free(item); return resp; -} \ No newline at end of file +} + +int CQ_GetWakeFd(const struct cq *q) { + if (!q) { + return -1; + } + return q->wake_fd; +} diff --git a/src/daemon/ipc_cq.h b/src/daemon/ipc_cq.h index 9f6660d..e2d50c6 100644 --- a/src/daemon/ipc_cq.h +++ b/src/daemon/ipc_cq.h @@ -13,6 +13,7 @@ struct cq_item { struct cq { struct cq_item *head; struct cq_item *tail; + int wake_fd; pthread_mutex_t lock; }; @@ -20,7 +21,8 @@ struct cq *CQ_Create(void); void CQ_Destroy(struct cq *q); void CQ_Push(struct cq *q, struct zvfs_resp *resp); struct zvfs_resp *CQ_Pop(struct cq *q); +int CQ_GetWakeFd(const struct cq *q); extern struct cq *g_cq; -#endif \ No newline at end of file +#endif diff --git a/src/daemon/ipc_reactor.c b/src/daemon/ipc_reactor.c index c27907f..e2a00be 100644 --- a/src/daemon/ipc_reactor.c +++ b/src/daemon/ipc_reactor.c @@ -1,6 +1,6 @@ #include "ipc_reactor.h" -#include "ipc_cq.h" #include "common/config.h" +#include "proto/ipc_proto.h" #include #include @@ -11,86 +11,183 @@ #include #include #include +#include #include #include +#include -static int send_all(int fd, const uint8_t *buf, size_t len) { - size_t off = 0; +static char g_reactor_wake_token; - while (off < len) { - ssize_t sent = send(fd, buf + off, len - off, 0); - if (sent > 0) { - off += (size_t)sent; - continue; - } - if (sent < 0 && errno == EINTR) { - continue; - } - if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - /* 当前实现优先功能,等待对端可写后重试。 */ - usleep(100); - continue; - } - return -1; - } - return 0; +static uint64_t now_mono_ns(void) { + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; } -/** ====================================================== */ -/* CQ OP */ -/** ====================================================== */ -static void cq_consume_send(struct cq *q) { - struct zvfs_resp *resp; - while ((resp = CQ_Pop(q)) != NULL) { - struct zvfs_conn *conn = resp->conn; - size_t cap = ZVFS_IPC_BUF_SIZE; - uint8_t *buf = NULL; +static void epoll_mod(struct zvfs_reactor *r, int fd, void *ptr, uint32_t events); - // printf("[resp][%s]\n",cast_opcode2string(resp->opcode)); +struct zvfs_pending_tx { + struct zvfs_pending_tx *next; + struct zvfs_conn *conn; + uint8_t *buf; + size_t len; + size_t off; + size_t trace_wake_write_patch_off; + size_t trace_reactor_wake_patch_off; + size_t trace_tx_patch_off; + int trace_wake_write_patched; + int trace_reactor_wake_patched; + int trace_tx_patched; +}; - buf = malloc(cap); - if (!buf) { - fprintf(stderr, "serialize resp failed: alloc %zu bytes\n", cap); - free(resp->data); - free(resp); - continue; - } +#define ZVFS_TX_TRACE_OFFSET_NONE ((size_t)-1) - size_t n = zvfs_serialize_resp(resp, buf, cap); - if (n == 0 && resp->status == 0 && resp->opcode == ZVFS_OP_READ) { - if (resp->length <= SIZE_MAX - 64) { - size_t need = (size_t)resp->length + 64; - uint8_t *bigger = realloc(buf, need); - if (bigger) { - buf = bigger; - cap = need; - n = zvfs_serialize_resp(resp, buf, cap); - } - } - } +enum trace_patch_field { + TRACE_PATCH_WAKE_WRITE = 0, + TRACE_PATCH_REACTOR_WAKE, + TRACE_PATCH_RESP_TX, +}; - if (n == 0) { - fprintf(stderr, "serialize resp failed: op=%u status=%d len=%lu cap=%zu\n", - resp->opcode, resp->status, resp->length, cap); - free(buf); - free(resp->data); - free(resp); - continue; - } - - if (send_all(conn->fd, buf, n) != 0) { - perror("send"); - free(buf); - free(resp->data); - free(resp); - continue; - } - free(buf); - - // 清理 - if(resp->data) free(resp->data); - free(resp); +static void free_pending_tx(struct zvfs_pending_tx *tx) { + if (!tx) { + return; } + if (tx->conn) { + zvfs_conn_put(tx->conn); + } + free(tx->buf); + free(tx); +} + +static void free_conn_tx_queue_locked(struct zvfs_conn *conn) { + struct zvfs_pending_tx *tx; + struct zvfs_pending_tx *next; + + for (tx = conn->tx_head; tx != NULL; tx = next) { + next = tx->next; + free_pending_tx(tx); + } + + conn->tx_head = NULL; + conn->tx_tail = NULL; +} + +static void conn_queue_tx_locked(struct zvfs_conn *conn, struct zvfs_pending_tx *tx) { + tx->next = NULL; + if (!conn->tx_tail) { + conn->tx_head = tx; + conn->tx_tail = tx; + return; + } + conn->tx_tail->next = tx; + conn->tx_tail = tx; +} + +static size_t resp_trace_patch_offset(const struct zvfs_resp *resp, enum trace_patch_field field) { + size_t base = ZVFS_RESP_HEADER_WIRE_SIZE; + size_t trace_base; + size_t index; + + if (!resp || (resp->trace.flags & ZVFS_RESP_TRACE_F_VALID) == 0) { + return ZVFS_TX_TRACE_OFFSET_NONE; + } + + switch (resp->opcode) { + case ZVFS_OP_WRITE: + trace_base = base + sizeof(uint64_t); + break; + case ZVFS_OP_SYNC_MD: + trace_base = base; + break; + default: + return ZVFS_TX_TRACE_OFFSET_NONE; + } + + switch (field) { + case TRACE_PATCH_WAKE_WRITE: + index = 6; + break; + case TRACE_PATCH_REACTOR_WAKE: + index = 7; + break; + case TRACE_PATCH_RESP_TX: + index = 8; + break; + default: + return ZVFS_TX_TRACE_OFFSET_NONE; + } + + return trace_base + sizeof(uint32_t) + sizeof(uint64_t) * index; +} + +static void patch_pending_tx_field(struct zvfs_pending_tx *tx, size_t off, int *patched) { + uint64_t ts; + + if (!tx || !patched || *patched || off == ZVFS_TX_TRACE_OFFSET_NONE) { + return; + } + if (off + sizeof(ts) > tx->len) { + *patched = 1; + return; + } + + ts = now_mono_ns(); + memcpy(tx->buf + off, &ts, sizeof(ts)); + *patched = 1; +} + +static void patch_pending_tx_trace(struct zvfs_pending_tx *tx) { + patch_pending_tx_field(tx, tx->trace_tx_patch_off, &tx->trace_tx_patched); +} + +static void patch_conn_head_reactor_wake_locked(struct zvfs_conn *conn) { + if (!conn || !conn->tx_head) { + return; + } + patch_pending_tx_field(conn->tx_head, + conn->tx_head->trace_reactor_wake_patch_off, + &conn->tx_head->trace_reactor_wake_patched); +} + +static int conn_flush_tx_locked(struct zvfs_conn *conn) { + while (conn->tx_head) { + struct zvfs_pending_tx *tx = conn->tx_head; + + patch_pending_tx_trace(tx); + + while (tx->off < tx->len) { + ssize_t sent = send(conn->fd, tx->buf + tx->off, tx->len - tx->off, 0); + + if (sent > 0) { + tx->off += (size_t)sent; + continue; + } + if (sent < 0 && errno == EINTR) { + continue; + } + if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + if (!conn->want_write) { + conn->want_write = 1; + epoll_mod(conn->reactor, conn->fd, conn, EPOLLIN | EPOLLOUT | EPOLLET); + } + return 0; + } + return -1; + } + + conn->tx_head = tx->next; + if (!conn->tx_head) { + conn->tx_tail = NULL; + } + free_pending_tx(tx); + } + + if (conn->want_write) { + conn->want_write = 0; + epoll_mod(conn->reactor, conn->fd, conn, EPOLLIN | EPOLLET); + } + return 0; } static int set_nonblock(int fd){ @@ -122,11 +219,94 @@ static void epoll_mod(struct zvfs_reactor *r, int fd, void *ptr, uint32_t events epoll_ctl(r->epfd, EPOLL_CTL_MOD, fd, &ev); } +static void reactor_wake(struct zvfs_reactor *r) { + uint64_t one = 1; + ssize_t n; + + if (!r || r->wake_fd < 0) { + return; + } + + do { + n = write(r->wake_fd, &one, sizeof(one)); + } while (n < 0 && errno == EINTR); +} + +static void reactor_drain_wakefd(struct zvfs_reactor *r) { + for (;;) { + uint64_t value; + ssize_t n; + + n = read(r->wake_fd, &value, sizeof(value)); + if (n == (ssize_t)sizeof(value)) { + continue; + } + if (n < 0 && errno == EINTR) { + continue; + } + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; + } + return; + } +} + +static void reactor_queue_ready_conn(struct zvfs_conn *conn) { + struct zvfs_reactor *r; + + if (!conn || !conn->reactor) { + return; + } + + r = conn->reactor; + zvfs_conn_get(conn); + + pthread_mutex_lock(&r->ready_lock); + conn->ready_next = NULL; + if (!r->ready_tail) { + r->ready_head = conn; + r->ready_tail = conn; + } else { + r->ready_tail->ready_next = conn; + r->ready_tail = conn; + } + pthread_mutex_unlock(&r->ready_lock); + + reactor_wake(r); +} + +static struct zvfs_conn *reactor_pop_ready_conn(struct zvfs_reactor *r) { + struct zvfs_conn *conn; + + pthread_mutex_lock(&r->ready_lock); + conn = r->ready_head; + if (conn) { + r->ready_head = conn->ready_next; + if (!r->ready_head) { + r->ready_tail = NULL; + } + conn->ready_next = NULL; + } + pthread_mutex_unlock(&r->ready_lock); + + return conn; +} + static void conn_destroy(struct zvfs_conn *c){ - close(c->fd); + pthread_mutex_destroy(&c->tx_lock); free(c); } +void zvfs_conn_get(struct zvfs_conn *conn){ + atomic_fetch_add_explicit(&conn->refcnt, 1, memory_order_relaxed); +} + +void zvfs_conn_put(struct zvfs_conn *conn){ + if (atomic_fetch_sub_explicit(&conn->refcnt, 1, memory_order_acq_rel) == 1) { + conn_destroy(conn); + } +} + int zvfs_conn_get_fd(struct zvfs_conn *conn){ return conn->fd; } @@ -140,38 +320,146 @@ void *zvfs_conn_get_ctx(struct zvfs_conn *conn){ } void zvfs_conn_enable_write(struct zvfs_conn *conn){ - if (conn->want_write) - return; - - conn->want_write = 1; - - struct zvfs_reactor *r = conn->reactor; - - epoll_mod(r, conn->fd, conn, - EPOLLIN | EPOLLOUT | EPOLLET); + pthread_mutex_lock(&conn->tx_lock); + if (!conn->closed && !conn->want_write) { + conn->want_write = 1; + epoll_mod(conn->reactor, conn->fd, conn, EPOLLIN | EPOLLOUT | EPOLLET); + } + pthread_mutex_unlock(&conn->tx_lock); } void zvfs_conn_disable_write(struct zvfs_conn *conn){ - if (!conn->want_write) - return; - - conn->want_write = 0; - - struct zvfs_reactor *r = conn->reactor; - - epoll_mod(r, conn->fd, conn, - EPOLLIN | EPOLLET); + pthread_mutex_lock(&conn->tx_lock); + if (!conn->closed && conn->want_write) { + conn->want_write = 0; + epoll_mod(conn->reactor, conn->fd, conn, EPOLLIN | EPOLLET); + } + pthread_mutex_unlock(&conn->tx_lock); } void zvfs_conn_close(struct zvfs_conn *conn){ struct zvfs_reactor *r = conn->reactor; + int fd = -1; - if (r->opts.on_close) + pthread_mutex_lock(&conn->tx_lock); + if (conn->closed) { + pthread_mutex_unlock(&conn->tx_lock); + return; + } + conn->closed = 1; + fd = conn->fd; + conn->fd = -1; + free_conn_tx_queue_locked(conn); + pthread_mutex_unlock(&conn->tx_lock); + + if (r->opts.on_close) { r->opts.on_close(conn, r->opts.cb_ctx); + } - epoll_ctl(r->epfd, EPOLL_CTL_DEL, conn->fd, NULL); + if (fd >= 0) { + epoll_ctl(r->epfd, EPOLL_CTL_DEL, fd, NULL); + close(fd); + } - conn_destroy(conn); + zvfs_conn_put(conn); +} + +int zvfs_conn_submit_resp(struct zvfs_resp *resp) { + struct zvfs_conn *conn; + struct zvfs_pending_tx *tx = NULL; + uint8_t *buf = NULL; + size_t cap = ZVFS_IPC_BUF_SIZE; + size_t n; + int need_schedule = 0; + int tx_conn_ref = 0; + + if (!resp || !resp->conn) { + return -1; + } + + conn = resp->conn; + buf = malloc(cap); + if (!buf) { + fprintf(stderr, "serialize resp failed: alloc %zu bytes\n", cap); + goto err; + } + + n = zvfs_serialize_resp(resp, buf, cap); + if (n == 0 && resp->status == 0 && resp->opcode == ZVFS_OP_READ) { + if (resp->length <= SIZE_MAX - 64) { + size_t need = (size_t)resp->length + 64; + uint8_t *bigger = realloc(buf, need); + + if (bigger) { + buf = bigger; + cap = need; + n = zvfs_serialize_resp(resp, buf, cap); + } + } + } + + if (n == 0) { + fprintf(stderr, "serialize resp failed: op=%u status=%d len=%lu cap=%zu\n", + resp->opcode, resp->status, resp->length, cap); + goto err; + } + + tx = calloc(1, sizeof(*tx)); + if (!tx) { + fprintf(stderr, "alloc pending tx failed: len=%zu\n", n); + goto err; + } + + tx->conn = conn; + tx->buf = buf; + tx->len = n; + tx->trace_wake_write_patch_off = resp_trace_patch_offset(resp, TRACE_PATCH_WAKE_WRITE); + tx->trace_reactor_wake_patch_off = resp_trace_patch_offset(resp, TRACE_PATCH_REACTOR_WAKE); + tx->trace_tx_patch_off = resp_trace_patch_offset(resp, TRACE_PATCH_RESP_TX); + tx->trace_wake_write_patched = 0; + tx->trace_reactor_wake_patched = 0; + tx->trace_tx_patched = 0; + zvfs_conn_get(conn); + tx_conn_ref = 1; + + pthread_mutex_lock(&conn->tx_lock); + if (conn->closed) { + pthread_mutex_unlock(&conn->tx_lock); + goto err; + } + + conn_queue_tx_locked(conn, tx); + if (!conn->want_write && !conn->queued_for_flush) { + conn->queued_for_flush = 1; + need_schedule = 1; + } + pthread_mutex_unlock(&conn->tx_lock); + + if (need_schedule) { + patch_pending_tx_field(tx, tx->trace_wake_write_patch_off, &tx->trace_wake_write_patched); + reactor_queue_ready_conn(conn); + } + + if (resp->data) { + free(resp->data); + } + free(resp); + zvfs_conn_put(conn); + return 0; + +err: + if (tx) { + tx->conn = tx_conn_ref ? conn : NULL; + free_pending_tx(tx); + buf = NULL; + } + free(buf); + if (resp->data) { + free(resp->data); + } + free(resp); + zvfs_conn_put(conn); + return -1; } /** @@ -214,14 +502,17 @@ struct zvfs_reactor *zvfs_reactor_create(const struct zvfs_reactor_opts *opts){ struct zvfs_reactor *r = calloc(1, sizeof(*r)); r->opts = *opts; + pthread_mutex_init(&r->ready_lock, NULL); r->epfd = epoll_create1(0); + r->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); r->listen_fd = create_listen_socket( opts->socket_path, opts->backlog); epoll_add(r, r->listen_fd, NULL, EPOLLIN); + epoll_add(r, r->wake_fd, &g_reactor_wake_token, EPOLLIN); return r; } @@ -245,6 +536,8 @@ static void handle_accept(struct zvfs_reactor *r){ conn->fd = fd; conn->reactor = r; + atomic_init(&conn->refcnt, 1); + pthread_mutex_init(&conn->tx_lock, NULL); epoll_add(r, fd, conn, EPOLLIN | EPOLLET); @@ -260,39 +553,70 @@ zvfs_reactor_run(struct zvfs_reactor *r){ r->running = 1; while (r->running) { - int n = epoll_wait(r->epfd, events, 64, 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + for (int i = 0; i < n; i++) { - if (events[i].data.ptr == NULL) { - handle_accept(r); continue; } + if (events[i].data.ptr == &g_reactor_wake_token) { + struct zvfs_conn *ready_conn; + + reactor_drain_wakefd(r); + while ((ready_conn = reactor_pop_ready_conn(r)) != NULL) { + pthread_mutex_lock(&ready_conn->tx_lock); + ready_conn->queued_for_flush = 0; + patch_conn_head_reactor_wake_locked(ready_conn); + if (!ready_conn->closed && conn_flush_tx_locked(ready_conn) != 0) { + pthread_mutex_unlock(&ready_conn->tx_lock); + zvfs_conn_close(ready_conn); + zvfs_conn_put(ready_conn); + continue; + } + pthread_mutex_unlock(&ready_conn->tx_lock); + zvfs_conn_put(ready_conn); + } + continue; + } + struct zvfs_conn *conn = events[i].data.ptr; if (events[i].events & (EPOLLHUP | EPOLLERR)) { - zvfs_conn_close(conn); continue; } - if ((events[i].events & EPOLLIN) && - r->opts.on_read) { - + if ((events[i].events & EPOLLIN) && r->opts.on_read) { r->opts.on_read(conn, r->opts.cb_ctx); } - if ((events[i].events & EPOLLOUT) && - r->opts.on_write) { + if (events[i].events & EPOLLOUT) { + int rc; + pthread_mutex_lock(&conn->tx_lock); + rc = conn->closed ? 0 : conn_flush_tx_locked(conn); + pthread_mutex_unlock(&conn->tx_lock); + if (rc != 0) { + zvfs_conn_close(conn); + continue; + } + } + + if ((events[i].events & EPOLLOUT) && r->opts.on_write) { r->opts.on_write(conn, r->opts.cb_ctx); } } - cq_consume_send(g_cq); } + return 0; } @@ -303,7 +627,8 @@ void zvfs_reactor_stop(struct zvfs_reactor *r){ void zvfs_reactor_destroy(struct zvfs_reactor *r){ close(r->listen_fd); + close(r->wake_fd); close(r->epfd); + pthread_mutex_destroy(&r->ready_lock); free(r); } - diff --git a/src/daemon/ipc_reactor.h b/src/daemon/ipc_reactor.h index fc0f49d..5dd18ae 100644 --- a/src/daemon/ipc_reactor.h +++ b/src/daemon/ipc_reactor.h @@ -3,6 +3,8 @@ #include #include +#include +#include #ifdef __cplusplus extern "C" { @@ -11,6 +13,8 @@ extern "C" { struct zvfs_reactor_opts; struct zvfs_conn; struct zvfs_reactor; +struct zvfs_pending_tx; +struct zvfs_resp; /* callbacks */ @@ -57,10 +61,19 @@ struct zvfs_conn { int fd; int want_write; + int closed; + int queued_for_flush; void *user_ctx; struct zvfs_reactor *reactor; + + pthread_mutex_t tx_lock; + atomic_int refcnt; + + struct zvfs_pending_tx *tx_head; + struct zvfs_pending_tx *tx_tail; + struct zvfs_conn *ready_next; }; struct zvfs_reactor { @@ -69,8 +82,14 @@ struct zvfs_reactor { int listen_fd; + int wake_fd; + int running; + pthread_mutex_t ready_lock; + struct zvfs_conn *ready_head; + struct zvfs_conn *ready_tail; + struct zvfs_reactor_opts opts; }; @@ -110,9 +129,18 @@ zvfs_conn_set_ctx(struct zvfs_conn *conn, void *ctx); void * zvfs_conn_get_ctx(struct zvfs_conn *conn); +void +zvfs_conn_get(struct zvfs_conn *conn); + +void +zvfs_conn_put(struct zvfs_conn *conn); + +int +zvfs_conn_submit_resp(struct zvfs_resp *resp); + #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif diff --git a/src/daemon/main.c b/src/daemon/main.c index 4b9ba55..6b90952 100644 --- a/src/daemon/main.c +++ b/src/daemon/main.c @@ -1,8 +1,9 @@ - +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif #include "common/config.h" #include "proto/ipc_proto.h" #include "ipc_reactor.h" -#include "ipc_cq.h" #include "spdk_engine_wrapper.h" #include @@ -13,6 +14,8 @@ #include #include #include +#include +#include // #define IPC_REACTOR_ECHO @@ -20,6 +23,192 @@ extern struct zvfs_spdk_io_engine g_engine; +enum conn_rx_stage { + CONN_RX_STAGE_HEADER = 0, + CONN_RX_STAGE_BODY, + CONN_RX_STAGE_WRITE_META, + CONN_RX_STAGE_WRITE_DATA, +}; + +struct conn_rx_ctx { + enum conn_rx_stage stage; + uint8_t header_buf[ZVFS_REQ_HEADER_WIRE_SIZE]; + size_t header_bytes; + struct zvfs_req_header header; + + struct zvfs_req *req; + + uint8_t *body_buf; + size_t body_len; + size_t body_bytes; + + uint8_t write_meta_buf[ZVFS_REQ_WRITE_FIXED_WIRE_SIZE]; + size_t write_meta_bytes; +}; + +static uint64_t now_mono_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +static void bind_current_thread_cpu(const char *name, int cpu_id) +{ + cpu_set_t set; + + CPU_ZERO(&set); + CPU_SET(cpu_id, &set); + + if (sched_setaffinity(0, sizeof(set), &set) != 0) { + fprintf(stderr, "[affinity] bind %s to cpu %d failed: %s\n", + name, cpu_id, strerror(errno)); + } +} + +static void conn_rx_reset(struct conn_rx_ctx *rctx) +{ + if (!rctx) { + return; + } + + free(rctx->body_buf); + rctx->body_buf = NULL; + rctx->body_len = 0; + rctx->body_bytes = 0; + rctx->write_meta_bytes = 0; + rctx->header_bytes = 0; + memset(&rctx->header, 0, sizeof(rctx->header)); + rctx->stage = CONN_RX_STAGE_HEADER; +} + +static void conn_rx_discard_req(struct conn_rx_ctx *rctx) +{ + if (!rctx) { + return; + } + + if (rctx->req) { + free(rctx->req->data); + free(rctx->req->add_ref_items); + free(rctx->req); + rctx->req = NULL; + } + + conn_rx_reset(rctx); +} + +static int conn_rx_req_ready(struct zvfs_conn *c, struct conn_rx_ctx *rctx) +{ + struct zvfs_req *req = rctx->req; + + req->trace_req_rx_ns = now_mono_ns(); + zvfs_conn_get(c); + req->conn = c; + req->trace_dispatch_ns = now_mono_ns(); + + rctx->req = NULL; + conn_rx_reset(rctx); + + if (dispatch_to_worker(req) < 0) { + fprintf(stderr, "[dispatcher] [fd:%d] dispatch error\n", c->fd); + } + + return 0; +} + +static int conn_rx_handle_write_meta(struct zvfs_conn *c, struct conn_rx_ctx *rctx) +{ + struct zvfs_req_write_body body; + + if (zvfs_deserialize_req_write_fixed(rctx->write_meta_buf, sizeof(rctx->write_meta_buf), &body) != + ZVFS_REQ_WRITE_FIXED_WIRE_SIZE) { + fprintf(stderr, "[read] malformed write meta fd=%d\n", c->fd); + return -1; + } + + rctx->req = calloc(1, sizeof(*rctx->req)); + if (!rctx->req) { + fprintf(stderr, "[read] alloc write req failed fd=%d\n", c->fd); + return -1; + } + + rctx->req->opcode = ZVFS_OP_WRITE; + rctx->req->handle_id = body.handle_id; + rctx->req->offset = body.offset; + rctx->req->length = body.length; + rctx->req->write_flags = body.flags; + + if (body.length > 0) { + rctx->req->data = malloc((size_t)body.length); + if (!rctx->req->data) { + fprintf(stderr, "[read] alloc write payload failed fd=%d len=%lu\n", + c->fd, (unsigned long)body.length); + return -1; + } + } + + rctx->body_len = (size_t)body.length; + rctx->body_bytes = 0; + if (rctx->body_len == 0) { + return conn_rx_req_ready(c, rctx); + } + + rctx->stage = CONN_RX_STAGE_WRITE_DATA; + return 0; +} + +static int conn_rx_prepare_next(struct zvfs_conn *c, struct conn_rx_ctx *rctx) +{ + struct zvfs_req *req; + + if (rctx->header.opcode < ZVFS_OP_CREATE || rctx->header.opcode > ZVFS_OP_RESET_BLOBSTORE) { + fprintf(stderr, "[read] invalid opcode fd=%d op=%u\n", c->fd, rctx->header.opcode); + return -1; + } + + if (rctx->header.opcode == ZVFS_OP_WRITE) { + if (rctx->header.payload_len < ZVFS_REQ_WRITE_FIXED_WIRE_SIZE) { + fprintf(stderr, "[read] short write payload fd=%d payload_len=%u\n", + c->fd, rctx->header.payload_len); + return -1; + } + rctx->stage = CONN_RX_STAGE_WRITE_META; + rctx->write_meta_bytes = 0; + return 0; + } + + req = calloc(1, sizeof(*req)); + if (!req) { + fprintf(stderr, "[read] alloc req failed fd=%d\n", c->fd); + return -1; + } + + rctx->req = req; + rctx->body_len = ZVFS_REQ_HEADER_WIRE_SIZE + rctx->header.payload_len; + rctx->body_bytes = 0; + rctx->body_buf = malloc(rctx->body_len); + if (!rctx->body_buf) { + fprintf(stderr, "[read] alloc body failed fd=%d len=%zu\n", c->fd, rctx->body_len); + return -1; + } + + memcpy(rctx->body_buf, rctx->header_buf, ZVFS_REQ_HEADER_WIRE_SIZE); + rctx->body_bytes = ZVFS_REQ_HEADER_WIRE_SIZE; + rctx->stage = CONN_RX_STAGE_BODY; + + if (rctx->body_len == ZVFS_REQ_HEADER_WIRE_SIZE) { + if (zvfs_deserialize_req(rctx->body_buf, rctx->body_len, rctx->req) == 0) { + fprintf(stderr, "[read] deserialize req failed fd=%d op=%u\n", c->fd, rctx->header.opcode); + return -1; + } + return conn_rx_req_ready(c, rctx); + } + + return 0; +} + #ifdef IPC_REACTOR_ECHO static void on_accept(struct zvfs_conn *conn, void *ctx) @@ -103,26 +292,14 @@ int main() #else static void on_accept(struct zvfs_conn *conn, void *ctx) { - struct { - uint8_t *buf; - size_t len; - size_t cap; - } *rctx = calloc(1, sizeof(*rctx)); + struct conn_rx_ctx *rctx = calloc(1, sizeof(*rctx)); if (!rctx) { fprintf(stderr, "[accept] alloc conn ctx failed\n"); zvfs_conn_close(conn); return; } - - rctx->cap = ZVFS_IPC_BUF_SIZE; - rctx->buf = calloc(1, rctx->cap); - if (!rctx->buf) { - fprintf(stderr, "[accept] alloc conn rx buffer failed\n"); - free(rctx); - zvfs_conn_close(conn); - return; - } + conn_rx_reset(rctx); zvfs_conn_set_ctx(conn, rctx); printf("client connected fd=%d\n", @@ -132,29 +309,47 @@ static void on_accept(struct zvfs_conn *conn, void *ctx) static void on_read(struct zvfs_conn *c, void *ctx) { int fd = zvfs_conn_get_fd(c); - struct { - uint8_t *buf; - size_t len; - size_t cap; - } *rctx = zvfs_conn_get_ctx(c); + struct conn_rx_ctx *rctx = zvfs_conn_get_ctx(c); - if (!rctx || !rctx->buf || rctx->cap == 0) { + if (!rctx) { fprintf(stderr, "[read] invalid conn ctx fd=%d\n", fd); zvfs_conn_close(c); return; } for (;;) { - if (rctx->len >= rctx->cap) { - fprintf(stderr, "[read] rx buffer overflow fd=%d len=%zu cap=%zu\n", - fd, rctx->len, rctx->cap); + ssize_t n; + size_t need; + void *dst; + + switch (rctx->stage) { + case CONN_RX_STAGE_HEADER: + need = sizeof(rctx->header_buf) - rctx->header_bytes; + dst = rctx->header_buf + rctx->header_bytes; + break; + case CONN_RX_STAGE_BODY: + need = rctx->body_len - rctx->body_bytes; + dst = rctx->body_buf + rctx->body_bytes; + break; + case CONN_RX_STAGE_WRITE_META: + need = sizeof(rctx->write_meta_buf) - rctx->write_meta_bytes; + dst = rctx->write_meta_buf + rctx->write_meta_bytes; + break; + case CONN_RX_STAGE_WRITE_DATA: + need = rctx->body_len - rctx->body_bytes; + dst = (uint8_t *)rctx->req->data + rctx->body_bytes; + break; + default: + fprintf(stderr, "[read] invalid stage fd=%d stage=%d\n", fd, (int)rctx->stage); + conn_rx_discard_req(rctx); zvfs_conn_close(c); return; } - ssize_t n = read(fd, rctx->buf + rctx->len, rctx->cap - rctx->len); + n = read(fd, dst, need); if (n == 0) { fprintf(stderr, "[read] fd=%d closed\n", fd); + conn_rx_discard_req(rctx); zvfs_conn_close(c); return; } @@ -162,63 +357,73 @@ static void on_read(struct zvfs_conn *c, void *ctx) if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("[read]"); + conn_rx_discard_req(rctx); zvfs_conn_close(c); return; } + return; + } + + switch (rctx->stage) { + case CONN_RX_STAGE_HEADER: + rctx->header_bytes += (size_t)n; + if (rctx->header_bytes == sizeof(rctx->header_buf)) { + if (zvfs_deserialize_req_header(rctx->header_buf, sizeof(rctx->header_buf), &rctx->header) == 0 || + conn_rx_prepare_next(c, rctx) != 0) { + conn_rx_discard_req(rctx); + zvfs_conn_close(c); + return; + } + } + break; + case CONN_RX_STAGE_BODY: + rctx->body_bytes += (size_t)n; + if (rctx->body_bytes == rctx->body_len) { + if (zvfs_deserialize_req(rctx->body_buf, rctx->body_len, rctx->req) == 0) { + fprintf(stderr, "[read] deserialize req failed fd=%d op=%u\n", fd, rctx->header.opcode); + conn_rx_discard_req(rctx); + zvfs_conn_close(c); + return; + } + if (conn_rx_req_ready(c, rctx) != 0) { + conn_rx_discard_req(rctx); + zvfs_conn_close(c); + return; + } + } + break; + case CONN_RX_STAGE_WRITE_META: + rctx->write_meta_bytes += (size_t)n; + if (rctx->write_meta_bytes == sizeof(rctx->write_meta_buf)) { + if (conn_rx_handle_write_meta(c, rctx) != 0) { + conn_rx_discard_req(rctx); + zvfs_conn_close(c); + return; + } + } + break; + case CONN_RX_STAGE_WRITE_DATA: + rctx->body_bytes += (size_t)n; + if (rctx->body_bytes == rctx->body_len) { + if (conn_rx_req_ready(c, rctx) != 0) { + conn_rx_discard_req(rctx); + zvfs_conn_close(c); + return; + } + } + break; + default: break; } - - rctx->len += (size_t)n; - } - - size_t offset = 0; - while (offset < rctx->len) { - struct zvfs_req *req = calloc(1, sizeof(*req)); - if (!req) { - fprintf(stderr, "malloc failed\n"); - break; - } - - size_t consumed = zvfs_deserialize_req(rctx->buf + offset, rctx->len - offset, req); - if (consumed == 0) { - free(req); - break; /* 等待更多数据 */ - } - - // printf("[req][%s]\n", cast_opcode2string(req->opcode)); - req->conn = c; - offset += consumed; - - if (dispatch_to_worker(req) < 0) { - fprintf(stderr, "[dispatcher] [fd:%d] dispatch error\n", c->fd); - } - } - - if (offset > 0) { - size_t remain = rctx->len - offset; - if (remain > 0) { - memmove(rctx->buf, rctx->buf + offset, remain); - } - rctx->len = remain; - } - - if (rctx->len == rctx->cap) { - fprintf(stderr, "[read] request too large or malformed fd=%d cap=%zu\n", - fd, rctx->cap); - zvfs_conn_close(c); } } static void on_close(struct zvfs_conn *conn, void *ctx) { - struct { - uint8_t *buf; - size_t len; - size_t cap; - } *rctx = zvfs_conn_get_ctx(conn); + struct conn_rx_ctx *rctx = zvfs_conn_get_ctx(conn); if (rctx) { - free(rctx->buf); + conn_rx_discard_req(rctx); free(rctx); zvfs_conn_set_ctx(conn, NULL); } @@ -229,14 +434,10 @@ static void on_close(struct zvfs_conn *conn, void *ctx) int main(void){ - - const char *bdev_name = getenv("SPDK_BDEV_NAME") ? getenv("SPDK_BDEV_NAME") : ZVFS_BDEV; const char *json_file = getenv("SPDK_JSON_CONFIG") ? getenv("SPDK_JSON_CONFIG") : SPDK_JSON_PATH; - - g_cq = CQ_Create(); - - zvfs_engine_init(bdev_name, json_file, 4); + zvfs_engine_init(bdev_name, json_file, ZVFS_IO_THREAD_COUNT + 1); + bind_current_thread_cpu("reactor", ZVFS_REACTOR_CPU); struct zvfs_reactor_opts opts = { .socket_path = ZVFS_IPC_DEFAULT_SOCKET_PATH, @@ -251,9 +452,5 @@ int main(void){ struct zvfs_reactor *r = zvfs_reactor_create(&opts); zvfs_reactor_run(r); - - - - if(g_cq) CQ_Destroy(g_cq); } #endif diff --git a/src/daemon/spdk_engine.c b/src/daemon/spdk_engine.c index a769acf..390156d 100644 --- a/src/daemon/spdk_engine.c +++ b/src/daemon/spdk_engine.c @@ -1,7 +1,10 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif #include "common/utils.h" #include "common/config.h" #include "spdk_engine.h" -#include "ipc_cq.h" +#include "ipc_reactor.h" #include "dma_buf_pool.h" #include @@ -15,6 +18,8 @@ #include #include #include +#include +#include /** =========================================================== * 全局引擎状态 @@ -25,10 +30,73 @@ static _Atomic bool g_bs_resetting = false; /** =========================================================== * 内部辅助:时钟 * ============================================================ */ -static uint64_t now_mono_ms(void) { +static uint64_t now_mono_ns(void) { struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +static uint64_t now_mono_ms(void) { + return now_mono_ns() / 1000000ULL; +} + +static void resp_trace_init_from_req(struct zvfs_resp *resp, const struct zvfs_req *req) { + if (!resp || !req) { + return; + } + + resp->trace.flags = ZVFS_RESP_TRACE_F_VALID; + resp->trace.req_rx_ns = req->trace_req_rx_ns; + resp->trace.dispatch_ns = req->trace_dispatch_ns; +} + +static void bind_current_thread_cpu(const char *name, int cpu_id) { + cpu_set_t set; + + CPU_ZERO(&set); + CPU_SET(cpu_id, &set); + + if (sched_setaffinity(0, sizeof(set), &set) != 0) { + SPDK_ERRLOG("bind %s to cpu %d failed: %s\n", + name, cpu_id, strerror(errno)); + return; + } + + SPDK_NOTICELOG("bound %s to cpu %d\n", name, cpu_id); +} + +static unsigned int poll_idle_us(void) { + static int inited = 0; + static unsigned int idle_us = 0; + const char *v; + char *end = NULL; + unsigned long parsed; + + if (!inited) { + v = getenv("ZVFS_SPDK_POLL_US"); + if (v && v[0] != '\0') { + parsed = strtoul(v, &end, 10); + if (end && *end == '\0' && parsed <= 1000000UL) { + idle_us = (unsigned int)parsed; + } + } + inited = 1; + } + + return idle_us; +} + +static void poll_thread_forever(struct spdk_thread *thread) { + const unsigned int idle_us = poll_idle_us(); + + while (true) { + int rc = spdk_thread_poll(thread, 0, 0); + + if (rc == 0 && idle_us > 0) { + usleep(idle_us); + } + } } /* @@ -50,6 +118,7 @@ static void push_err_resp(struct zvfs_req *req, int status) { struct zvfs_resp *resp = calloc(1, sizeof(*resp)); if (!resp) { SPDK_ERRLOG("push_err_resp: calloc failed, op_code=%u\n", req->opcode); + zvfs_conn_put(req->conn); if (req->data) free(req->data); if (req->add_ref_items) free(req->add_ref_items); free(req); @@ -61,7 +130,7 @@ static void push_err_resp(struct zvfs_req *req, int status) { if (req->data) free(req->data); if (req->add_ref_items) free(req->add_ref_items); free(req); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } /** =========================================================== @@ -188,6 +257,7 @@ static void *md_poller_fn(void *arg) { struct thread_bootstrap_ctx *boot = arg; struct zvfs_io_thread *slot = &g_engine.thread_pool[0]; + bind_current_thread_cpu("md_thread", ZVFS_MD_CPU); spdk_set_thread(slot->thread); int rc = load_json_config(slot->thread, boot->json_file); @@ -206,10 +276,7 @@ notify: slot->ready = true; /* 持续 poll,处理所有通过 spdk_thread_send_msg 分发的 md 操作 */ - while (true) { - spdk_thread_poll(slot->thread, 0, 0); - usleep(100); - } + poll_thread_forever(slot->thread); return NULL; } @@ -217,8 +284,10 @@ notify: static void *io_poller_fn(void *arg) { struct thread_bootstrap_ctx *boot = arg; int idx = boot->idx; + int cpu_id = ZVFS_IO_CPU_START + (idx - 1); struct zvfs_io_thread *slot = &g_engine.thread_pool[idx]; + bind_current_thread_cpu("io_thread", cpu_id); spdk_set_thread(slot->thread); /* 等待 md 线程完成 blobstore 初始化 */ @@ -252,10 +321,7 @@ static void *io_poller_fn(void *arg) { slot->ready = true; /* 持续 poll,处理通过 spdk_thread_send_msg 分发的 IO 操作 */ - while (true) { - spdk_thread_poll(slot->thread, 0, 0); - usleep(100); - } + poll_thread_forever(slot->thread); return NULL; } @@ -447,6 +513,7 @@ struct resize_ctx { struct sync_ctx { struct zvfs_req *req; struct zvfs_blob_handle *handle; + uint64_t spdk_start_ns; }; struct close_ctx { @@ -471,6 +538,8 @@ struct io_ctx { uint64_t lba_len; uint32_t buf_off; void *dma_buf; + uint64_t spdk_start_ns; + uint64_t phase1_done_ns; }; struct write_autogrow_ctx { @@ -589,7 +658,7 @@ static void create_sync_cb(void *arg, int bserrno) { free(cctx->req); free(cctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } int blob_create(struct zvfs_req *req) { @@ -646,7 +715,7 @@ static void blob_open_done_cb(void *arg, struct spdk_blob *blob, int bserrno) { free(octx->req); free(octx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void do_blob_open(void *arg) { @@ -678,7 +747,7 @@ static void blob_resize_done_cb(void *arg, int bserrno) { free(rctx->req); free(rctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void do_blob_resize(void *arg) { @@ -712,14 +781,19 @@ static void blob_sync_md_done_cb(void *arg, int bserrno) { resp->opcode = sctx->req->opcode; resp->status = bserrno; resp->conn = sctx->req->conn; + resp_trace_init_from_req(resp, sctx->req); + resp->trace.spdk_start_ns = sctx->spdk_start_ns; + resp->trace.spdk_done_ns = now_mono_ns(); + resp->trace.cq_push_ns = now_mono_ns(); free(sctx->req); free(sctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void do_blob_sync_md(void *arg) { struct sync_ctx *sctx = arg; + sctx->spdk_start_ns = now_mono_ns(); spdk_blob_sync_md(sctx->handle->blob, blob_sync_md_done_cb, sctx); } @@ -753,7 +827,7 @@ static void blob_close_done_cb(void *arg, int bserrno) { free(cctx->req); free(cctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void do_blob_close(void *arg) { @@ -782,7 +856,7 @@ int blob_close(struct zvfs_req *req) { resp->status = 0; resp->conn = req->conn; free(req); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); return 0; } break; @@ -810,7 +884,7 @@ static void blob_delete_done_cb(void *arg, int bserrno) { free(dctx->req); free(dctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void do_blob_delete(void *arg) { @@ -848,7 +922,7 @@ static void blobstore_reset_finish(struct reset_ctx *rctx, int status) { free(rctx->req); free(rctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); atomic_store(&g_bs_resetting, false); } @@ -951,7 +1025,7 @@ static void blob_read_done_cb(void *arg, int bserrno) { free(ioctx->req); free(ioctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void do_blob_read(void *arg) { @@ -1036,10 +1110,18 @@ static void blob_write_writephase_cb(void *arg, int bserrno) { resp->status = 0; resp->conn = ioctx->req->conn; resp->bytes_written = ioctx->req->length; + resp_trace_init_from_req(resp, ioctx->req); + resp->trace.spdk_start_ns = ioctx->spdk_start_ns; + resp->trace.phase1_done_ns = ioctx->phase1_done_ns; + resp->trace.spdk_done_ns = now_mono_ns(); + if (ioctx->phase1_done_ns != 0) { + resp->trace.flags |= ZVFS_RESP_TRACE_F_PHASE1_VALID; + } + resp->trace.cq_push_ns = now_mono_ns(); free(ioctx->req); free(ioctx); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void blob_write_readphase_cb(void *arg, int bserrno) { @@ -1052,6 +1134,8 @@ static void blob_write_readphase_cb(void *arg, int bserrno) { return; } + ioctx->phase1_done_ns = now_mono_ns(); + /* read-modify: 将用户数据覆盖到 dma_buf 的对应区域 */ memcpy((uint8_t *)ioctx->dma_buf + ioctx->buf_off, ioctx->req->data, ioctx->req->length); @@ -1060,10 +1144,18 @@ static void blob_write_readphase_cb(void *arg, int bserrno) { ioctx->lba_off, ioctx->lba_len, blob_write_writephase_cb, ioctx); } +static void blob_write_direct_submit(struct io_ctx *ioctx) { + memcpy(ioctx->dma_buf, ioctx->req->data, (size_t)ioctx->req->length); + spdk_blob_io_write(ioctx->handle->blob, ioctx->channel, ioctx->dma_buf, + ioctx->lba_off, ioctx->lba_len, blob_write_writephase_cb, ioctx); +} + static void do_blob_write(void *arg) { struct io_ctx *ioctx = arg; struct zvfs_blob_handle *handle = ioctx->handle; + ioctx->spdk_start_ns = now_mono_ns(); + uint64_t end = 0; if (__builtin_add_overflow(ioctx->req->offset, ioctx->req->length, &end)) { push_err_resp(ioctx->req, -EOVERFLOW); @@ -1104,7 +1196,13 @@ static void do_blob_write(void *arg) { ioctx->lba_len = lba_len; ioctx->buf_off = buf_off; ioctx->dma_buf = dma_buf; - + + if (buf_off == 0 && + ioctx->req->length == lba_len * g_engine.io_unit_size) { + blob_write_direct_submit(ioctx); + return; + } + spdk_blob_io_read(handle->blob, ioctx->channel, ioctx->dma_buf, lba_off, lba_len, blob_write_readphase_cb, ioctx); } diff --git a/src/daemon/spdk_engine_wrapper.c b/src/daemon/spdk_engine_wrapper.c index 8b7e6b3..be78f53 100644 --- a/src/daemon/spdk_engine_wrapper.c +++ b/src/daemon/spdk_engine_wrapper.c @@ -1,6 +1,6 @@ #include "spdk_engine_wrapper.h" #include "spdk_engine.h" -#include "ipc_cq.h" +#include "ipc_reactor.h" #include #include @@ -11,6 +11,7 @@ static void push_err_resp(struct zvfs_req *req, int status) { struct zvfs_resp *resp = calloc(1, sizeof(*resp)); if (!resp) { SPDK_ERRLOG("push_err_resp: calloc failed, op_code=%u\n", req->opcode); + zvfs_conn_put(req->conn); if (req->data) free(req->data); if (req->add_ref_items) free(req->add_ref_items); free(req); @@ -22,13 +23,14 @@ static void push_err_resp(struct zvfs_req *req, int status) { if (req->data) free(req->data); if (req->add_ref_items) free(req->add_ref_items); free(req); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } static void push_ok_resp(struct zvfs_req *req) { struct zvfs_resp *resp = calloc(1, sizeof(*resp)); if (!resp) { SPDK_ERRLOG("push_ok_resp: calloc failed, op_code=%u\n", req->opcode); + zvfs_conn_put(req->conn); if (req->data) free(req->data); if (req->add_ref_items) free(req->add_ref_items); free(req); @@ -40,7 +42,7 @@ static void push_ok_resp(struct zvfs_req *req) { if (req->data) free(req->data); if (req->add_ref_items) free(req->add_ref_items); free(req); - CQ_Push(g_cq, resp); + zvfs_conn_submit_resp(resp); } /** hash map op */ diff --git a/src/daemon/zvfs_daemon b/src/daemon/zvfs_daemon index 3b9665e..b224b1e 100755 Binary files a/src/daemon/zvfs_daemon and b/src/daemon/zvfs_daemon differ diff --git a/src/hook/zvfs_hook_fd.c b/src/hook/zvfs_hook_fd.c index 8774a99..17f8efe 100644 --- a/src/hook/zvfs_hook_fd.c +++ b/src/hook/zvfs_hook_fd.c @@ -19,96 +19,7 @@ #include #include -/* ------------------------------------------------------------------ */ -/* 内部:open/openat 调试日志 */ -/* ------------------------------------------------------------------ */ - -static inline const char * -zvfs_dbg_str(const char *s) -{ - return s ? s : "(null)"; -} - -static int -zvfs_debug_open_enabled(void) -{ - static int inited = 0; - static int enabled = 0; - const char *v; - - if (!inited) { - v = getenv("ZVFS_DEBUG_OPEN"); - enabled = (v && v[0] != '\0' && strcmp(v, "0") != 0); - inited = 1; - } - return enabled; -} - -static const char * -zvfs_debug_open_match(void) -{ - static int inited = 0; - static const char *match = NULL; - - if (!inited) { - match = getenv("ZVFS_DEBUG_OPEN_MATCH"); - if (match && match[0] == '\0') { - match = NULL; - } - inited = 1; - } - return match; -} - -static int -zvfs_debug_open_should_log(const char *path1, const char *path2) -{ - const char *match; - - if (!zvfs_debug_open_enabled()) { - return 0; - } - - match = zvfs_debug_open_match(); - if (!match) { - return 1; - } - - if (path1 && strstr(path1, match)) { - return 1; - } - if (path2 && strstr(path2, match)) { - return 1; - } - return 0; -} - -static void -zvfs_debug_open_log(const char *path1, const char *path2, const char *fmt, ...) -{ - va_list ap; - - if (!zvfs_debug_open_should_log(path1, path2)) { - return; - } - - fprintf(stderr, "[zvfs][open-dbg][pid=%d][tid=%lu] ", - getpid(), (unsigned long)pthread_self()); - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fputc('\n', stderr); -} - -static int -zvfs_debug_has_fd_mapping(int fd) -{ - int found = 0; - pthread_mutex_lock(&g_fs.fd_mu); - found = (openfile_lookup(fd) != NULL); - pthread_mutex_unlock(&g_fs.fd_mu); - return found; -} +#define zvfs_debug_open_log(...) ((void)0) /* close 路径辅助:在文件后半段实现。 */ static int zvfs_detach_fd_mapping(int fd, int do_sync_md); @@ -393,10 +304,6 @@ zvfs_open_impl(int real_fd, const char *abspath, int flags, mode_t mode) /* 未命中:从 xattr 读 blob_id,可能是进程首次 open */ if (zvfs_xattr_read_blob_id(real_fd, &blob_id) < 0) { /* xattr 不存在:不是 zvfs 管理的文件,降级透传 */ - int saved = errno; - zvfs_debug_open_log(abspath, NULL, - "open existing xattr_read miss errno=%d(%s), passthrough real_fd=%d", - saved, strerror(saved), real_fd); return real_fd; /* 直接返回,不做任何包装 */ } zvfs_debug_open_log(abspath, NULL, diff --git a/src/hook/zvfs_hook_rw.c b/src/hook/zvfs_hook_rw.c index fe3b341..3c20b6a 100644 --- a/src/hook/zvfs_hook_rw.c +++ b/src/hook/zvfs_hook_rw.c @@ -17,106 +17,10 @@ #include #include #include -#include #include #include -/* ------------------------------------------------------------------ */ -/* 内部:read/pread 调试日志 */ -/* ------------------------------------------------------------------ */ - -static int -zvfs_debug_rw_enabled(void) -{ - static int inited = 0; - static int enabled = 0; - const char *v; - - if (!inited) { - v = getenv("ZVFS_DEBUG_RW"); - enabled = (v && v[0] != '\0' && strcmp(v, "0") != 0); - inited = 1; - } - return enabled; -} - -static int -zvfs_debug_rw_fd_match(int fd) -{ - static int inited = 0; - static int has_filter = 0; - static int filter_fd = -1; - const char *v; - char *end = NULL; - long parsed; - - if (!inited) { - v = getenv("ZVFS_DEBUG_RW_FD"); - if (v && v[0] != '\0') { - parsed = strtol(v, &end, 10); - if (end && *end == '\0' && parsed >= 0 && parsed <= INT_MAX) { - has_filter = 1; - filter_fd = (int)parsed; - } - } - inited = 1; - } - - if (!has_filter) { - return 1; - } - return fd == filter_fd; -} - -static void -zvfs_debug_rw_log(int fd, const char *fmt, ...) -{ - va_list ap; - - if (!zvfs_debug_rw_enabled() || !zvfs_debug_rw_fd_match(fd)) { - return; - } - - fprintf(stderr, "[zvfs][rw-dbg][pid=%d][tid=%lu][fd=%d] ", - getpid(), (unsigned long)pthread_self(), fd); - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fputc('\n', stderr); -} - -static const char * -zvfs_debug_fd_target(int fd) -{ - static __thread char target[PATH_MAX]; - char linkpath[64]; - ssize_t n; - - if (fd < 0) { - return "(invalid-fd)"; - } - - snprintf(linkpath, sizeof(linkpath), "/proc/self/fd/%d", fd); - n = readlink(linkpath, target, sizeof(target) - 1); - if (n < 0) { - return "(unknown)"; - } - target[n] = '\0'; - return target; -} - -static uint64_t -zvfs_debug_logical_size(struct zvfs_open_file *of) -{ - uint64_t size; - if (!of || !of->inode) { - return 0; - } - pthread_mutex_lock(&of->inode->mu); - size = of->inode->logical_size; - pthread_mutex_unlock(&of->inode->mu); - return size; -} +#define zvfs_debug_rw_log(...) ((void)0) /* ------------------------------------------------------------------ */ /* 内部:单段 pread / pwrite(不修改 of->offset) */ @@ -420,7 +324,11 @@ zvfs_vfscanf_impl(FILE *stream, const char *format, va_list ap, int use_isoc99) } uint64_t cur_off = of->offset; - uint64_t logical_size = zvfs_debug_logical_size(of); + uint64_t logical_size; + + pthread_mutex_lock(&of->inode->mu); + logical_size = of->inode->logical_size; + pthread_mutex_unlock(&of->inode->mu); uint64_t remain64 = (cur_off < logical_size) ? (logical_size - cur_off) : 0; if (remain64 == 0) { stream->_flags |= _IO_EOF_SEEN; @@ -516,7 +424,6 @@ read(int fd, void *buf, size_t count) { ZVFS_HOOK_ENTER(); - int in_hook = ZVFS_IN_HOOK(); struct zvfs_open_file *of = get_of(fd); if (!of) { @@ -573,7 +480,6 @@ pread(int fd, void *buf, size_t count, off_t offset) { ZVFS_HOOK_ENTER(); - int in_hook = ZVFS_IN_HOOK(); struct zvfs_open_file *of = get_of(fd); if (!of) { diff --git a/src/proto/ipc_proto.c b/src/proto/ipc_proto.c index 72ffe6d..3d3a617 100644 --- a/src/proto/ipc_proto.c +++ b/src/proto/ipc_proto.c @@ -3,9 +3,6 @@ #include #include -#define ZVFS_REQ_HEADER_WIRE_SIZE (sizeof(uint32_t) + sizeof(uint32_t)) -#define ZVFS_RESP_HEADER_WIRE_SIZE (sizeof(uint32_t) + sizeof(int32_t) + sizeof(uint32_t)) - static int write_bytes(uint8_t **p, size_t *remaining, const void *src, size_t n) { if (*remaining < n) { return -1; @@ -50,6 +47,50 @@ static int read_u64(const uint8_t **p, size_t *remaining, uint64_t *v) { return read_bytes(p, remaining, v, sizeof(*v)); } +static int write_resp_trace(uint8_t **p, size_t *remaining, const struct zvfs_resp_trace *trace) { + struct zvfs_resp_trace zero = {0}; + + if (!trace) { + trace = &zero; + } + + if (write_u32(p, remaining, trace->flags) != 0 || + write_u64(p, remaining, trace->req_rx_ns) != 0 || + write_u64(p, remaining, trace->dispatch_ns) != 0 || + write_u64(p, remaining, trace->spdk_start_ns) != 0 || + write_u64(p, remaining, trace->phase1_done_ns) != 0 || + write_u64(p, remaining, trace->spdk_done_ns) != 0 || + write_u64(p, remaining, trace->cq_push_ns) != 0 || + write_u64(p, remaining, trace->wake_write_ns) != 0 || + write_u64(p, remaining, trace->reactor_wake_ns) != 0 || + write_u64(p, remaining, trace->resp_tx_ns) != 0) { + return -1; + } + + return 0; +} + +static int read_resp_trace(const uint8_t **p, size_t *remaining, struct zvfs_resp_trace *trace) { + if (!trace) { + return -1; + } + + if (read_u32(p, remaining, &trace->flags) != 0 || + read_u64(p, remaining, &trace->req_rx_ns) != 0 || + read_u64(p, remaining, &trace->dispatch_ns) != 0 || + read_u64(p, remaining, &trace->spdk_start_ns) != 0 || + read_u64(p, remaining, &trace->phase1_done_ns) != 0 || + read_u64(p, remaining, &trace->spdk_done_ns) != 0 || + read_u64(p, remaining, &trace->cq_push_ns) != 0 || + read_u64(p, remaining, &trace->wake_write_ns) != 0 || + read_u64(p, remaining, &trace->reactor_wake_ns) != 0 || + read_u64(p, remaining, &trace->resp_tx_ns) != 0) { + return -1; + } + + return 0; +} + static int valid_opcode(uint32_t opcode) { return opcode >= ZVFS_OP_CREATE && opcode <= ZVFS_OP_RESET_BLOBSTORE; } @@ -220,13 +261,45 @@ size_t zvfs_serialize_req_write(const struct zvfs_req_write_body *body, uint8_t if (body->length > 0 && !body->data) { return 0; } + if (zvfs_serialize_req_write_fixed(body, buf, buf_len) != ZVFS_REQ_WRITE_FIXED_WIRE_SIZE) { + return 0; + } + p += ZVFS_REQ_WRITE_FIXED_WIRE_SIZE; + remaining -= ZVFS_REQ_WRITE_FIXED_WIRE_SIZE; + if (body->length > 0 && write_bytes(&p, &remaining, body->data, (size_t)body->length) != 0) { + return 0; + } + return (size_t)(p - buf); +} + +size_t zvfs_serialize_req_write_fixed(const struct zvfs_req_write_body *body, uint8_t *buf, size_t buf_len) { + uint8_t *p = buf; + size_t remaining = buf_len; + + if (!body || !buf) { + return 0; + } if (write_u64(&p, &remaining, body->handle_id) != 0 || write_u64(&p, &remaining, body->offset) != 0 || write_u64(&p, &remaining, body->length) != 0 || write_u32(&p, &remaining, body->flags) != 0) { return 0; } - if (body->length > 0 && write_bytes(&p, &remaining, body->data, (size_t)body->length) != 0) { + return (size_t)(p - buf); +} + +size_t zvfs_deserialize_req_write_fixed(const uint8_t *buf, size_t buf_len, struct zvfs_req_write_body *body) { + const uint8_t *p = buf; + size_t remaining = buf_len; + + if (!body || !buf) { + return 0; + } + memset(body, 0, sizeof(*body)); + if (read_u64(&p, &remaining, &body->handle_id) != 0 || + read_u64(&p, &remaining, &body->offset) != 0 || + read_u64(&p, &remaining, &body->length) != 0 || + read_u32(&p, &remaining, &body->flags) != 0) { return 0; } return (size_t)(p - buf); @@ -239,14 +312,11 @@ size_t zvfs_deserialize_req_write(const uint8_t *buf, size_t buf_len, struct zvf if (!body || !buf) { return 0; } - body->data = NULL; - body->flags = 0; - if (read_u64(&p, &remaining, &body->handle_id) != 0 || - read_u64(&p, &remaining, &body->offset) != 0 || - read_u64(&p, &remaining, &body->length) != 0 || - read_u32(&p, &remaining, &body->flags) != 0) { + if (zvfs_deserialize_req_write_fixed(buf, buf_len, body) != ZVFS_REQ_WRITE_FIXED_WIRE_SIZE) { return 0; } + p += ZVFS_REQ_WRITE_FIXED_WIRE_SIZE; + remaining -= ZVFS_REQ_WRITE_FIXED_WIRE_SIZE; if (body->length > remaining) { return 0; @@ -570,7 +640,8 @@ size_t zvfs_serialize_resp_write(const struct zvfs_resp_write_body *body, uint8_ if (!body || !buf) { return 0; } - if (write_u64(&p, &remaining, body->bytes_written) != 0) { + if (write_u64(&p, &remaining, body->bytes_written) != 0 || + write_resp_trace(&p, &remaining, &body->trace) != 0) { return 0; } return (size_t)(p - buf); @@ -583,7 +654,10 @@ size_t zvfs_deserialize_resp_write(const uint8_t *buf, size_t buf_len, struct zv if (!body || !buf) { return 0; } - if (read_u64(&p, &remaining, &body->bytes_written) != 0) { + memset(body, 0, sizeof(*body)); + + if (read_u64(&p, &remaining, &body->bytes_written) != 0 || + read_resp_trace(&p, &remaining, &body->trace) != 0) { return 0; } return (size_t)(p - buf); @@ -601,16 +675,31 @@ size_t zvfs_deserialize_resp_resize(const uint8_t *buf, size_t buf_len) { return 0; } -size_t zvfs_serialize_resp_sync_md(uint8_t *buf, size_t buf_len) { - (void)buf; - (void)buf_len; - return 0; +size_t zvfs_serialize_resp_sync_md(const struct zvfs_resp_sync_md_body *body, uint8_t *buf, size_t buf_len) { + uint8_t *p = buf; + size_t remaining = buf_len; + + if (!body || !buf) { + return 0; + } + if (write_resp_trace(&p, &remaining, &body->trace) != 0) { + return 0; + } + return (size_t)(p - buf); } -size_t zvfs_deserialize_resp_sync_md(const uint8_t *buf, size_t buf_len) { - (void)buf; - (void)buf_len; - return 0; +size_t zvfs_deserialize_resp_sync_md(const uint8_t *buf, size_t buf_len, struct zvfs_resp_sync_md_body *body) { + const uint8_t *p = buf; + size_t remaining = buf_len; + + if (!body || !buf) { + return 0; + } + memset(body, 0, sizeof(*body)); + if (read_resp_trace(&p, &remaining, &body->trace) != 0) { + return 0; + } + return (size_t)(p - buf); } size_t zvfs_serialize_resp_close(uint8_t *buf, size_t buf_len) { @@ -935,13 +1024,26 @@ size_t zvfs_serialize_resp(struct zvfs_resp *resp, uint8_t *buf, size_t buf_len) break; } case ZVFS_OP_WRITE: { - struct zvfs_resp_write_body body = { .bytes_written = resp->bytes_written }; + struct zvfs_resp_write_body body = { + .bytes_written = resp->bytes_written, + .trace = resp->trace, + }; body_len = zvfs_serialize_resp_write(&body, buf + ZVFS_RESP_HEADER_WIRE_SIZE, buf_len - ZVFS_RESP_HEADER_WIRE_SIZE); break; } case ZVFS_OP_RESIZE: + body_len = 0; + break; case ZVFS_OP_SYNC_MD: + { + struct zvfs_resp_sync_md_body body = { + .trace = resp->trace, + }; + body_len = zvfs_serialize_resp_sync_md(&body, buf + ZVFS_RESP_HEADER_WIRE_SIZE, + buf_len - ZVFS_RESP_HEADER_WIRE_SIZE); + } + break; case ZVFS_OP_CLOSE: case ZVFS_OP_DELETE: case ZVFS_OP_ADD_REF: @@ -1036,11 +1138,24 @@ size_t zvfs_deserialize_resp(uint8_t *buf, size_t buf_len, struct zvfs_resp *res consumed = zvfs_deserialize_resp_write(payload, header.payload_len, &body); if (consumed == header.payload_len) { resp->bytes_written = body.bytes_written; + resp->trace = body.trace; } break; } case ZVFS_OP_RESIZE: - case ZVFS_OP_SYNC_MD: + if (header.payload_len != 0) { + return 0; + } + consumed = 0; + break; + case ZVFS_OP_SYNC_MD: { + struct zvfs_resp_sync_md_body body; + consumed = zvfs_deserialize_resp_sync_md(payload, header.payload_len, &body); + if (consumed == header.payload_len) { + resp->trace = body.trace; + } + break; + } case ZVFS_OP_CLOSE: case ZVFS_OP_DELETE: case ZVFS_OP_ADD_REF: diff --git a/src/proto/ipc_proto.h b/src/proto/ipc_proto.h index dc509b3..a7eeedb 100644 --- a/src/proto/ipc_proto.h +++ b/src/proto/ipc_proto.h @@ -69,6 +69,13 @@ inline const char* cast_opcode2string(uint32_t op){ #define ZVFS_WRITE_F_AUTO_GROW (1u << 0) +#define ZVFS_RESP_TRACE_F_VALID (1u << 0) +#define ZVFS_RESP_TRACE_F_PHASE1_VALID (1u << 1) + +#define ZVFS_REQ_HEADER_WIRE_SIZE (sizeof(uint32_t) + sizeof(uint32_t)) +#define ZVFS_RESP_HEADER_WIRE_SIZE (sizeof(uint32_t) + sizeof(int32_t) + sizeof(uint32_t)) +#define ZVFS_REQ_WRITE_FIXED_WIRE_SIZE (sizeof(uint64_t) * 3 + sizeof(uint32_t)) + /* 最小固定头(同步阻塞场景,不含 request_id) */ struct zvfs_req_header { uint32_t opcode; @@ -154,15 +161,33 @@ struct zvfs_resp_read_body { void *data; }; +struct zvfs_resp_trace { + uint32_t flags; + uint64_t req_rx_ns; + uint64_t dispatch_ns; + uint64_t spdk_start_ns; + uint64_t phase1_done_ns; + uint64_t spdk_done_ns; + uint64_t cq_push_ns; + uint64_t wake_write_ns; + uint64_t reactor_wake_ns; + uint64_t resp_tx_ns; +}; + struct zvfs_resp_write_body { uint64_t bytes_written; + struct zvfs_resp_trace trace; +}; + +struct zvfs_resp_sync_md_body { + struct zvfs_resp_trace trace; }; /* resize/sync_md/close/delete 成功时 body 为空 */ size_t zvfs_serialize_resp_resize(uint8_t *buf, size_t buf_len); size_t zvfs_deserialize_resp_resize(const uint8_t *buf, size_t buf_len); -size_t zvfs_serialize_resp_sync_md(uint8_t *buf, size_t buf_len); -size_t zvfs_deserialize_resp_sync_md(const uint8_t *buf, size_t buf_len); +size_t zvfs_serialize_resp_sync_md(const struct zvfs_resp_sync_md_body *body, uint8_t *buf, size_t buf_len); +size_t zvfs_deserialize_resp_sync_md(const uint8_t *buf, size_t buf_len, struct zvfs_resp_sync_md_body *body); size_t zvfs_serialize_resp_close(uint8_t *buf, size_t buf_len); size_t zvfs_deserialize_resp_close(const uint8_t *buf, size_t buf_len); size_t zvfs_serialize_resp_delete(uint8_t *buf, size_t buf_len); @@ -188,6 +213,9 @@ struct zvfs_req { struct zvfs_conn *conn; struct zvfs_blob_handle *handle; + + uint64_t trace_req_rx_ns; + uint64_t trace_dispatch_ns; }; struct zvfs_resp { @@ -202,6 +230,7 @@ struct zvfs_resp { void *data; uint64_t bytes_written; + struct zvfs_resp_trace trace; struct zvfs_conn *conn; }; @@ -226,6 +255,8 @@ size_t zvfs_serialize_req_read(const struct zvfs_req_read_body *body, uint8_t *b size_t zvfs_deserialize_req_read(const uint8_t *buf, size_t buf_len, struct zvfs_req_read_body *body); size_t zvfs_serialize_req_write(const struct zvfs_req_write_body *body, uint8_t *buf, size_t buf_len); +size_t zvfs_serialize_req_write_fixed(const struct zvfs_req_write_body *body, uint8_t *buf, size_t buf_len); +size_t zvfs_deserialize_req_write_fixed(const uint8_t *buf, size_t buf_len, struct zvfs_req_write_body *body); size_t zvfs_deserialize_req_write(const uint8_t *buf, size_t buf_len, struct zvfs_req_write_body *body); size_t zvfs_serialize_req_resize(const struct zvfs_req_resize_body *body, uint8_t *buf, size_t buf_len); diff --git a/src/spdk_engine/io_engine.c b/src/spdk_engine/io_engine.c index d3f47ad..5689166 100644 --- a/src/spdk_engine/io_engine.c +++ b/src/spdk_engine/io_engine.c @@ -8,8 +8,11 @@ #include #include #include +#include #include +#include #include +#include #include @@ -28,6 +31,154 @@ static __thread struct ipc_client_ctx g_ipc_tls = { .rx_len = 0, }; +static uint64_t now_mono_ns(void) { + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +static int latency_trace_enabled(void) { + static int inited = 0; + static int enabled = 0; + const char *v; + + if (!inited) { + v = getenv("ZVFS_TRACE_LATENCY"); + enabled = (v && v[0] != '\0' && strcmp(v, "0") != 0); + inited = 1; + } + + return enabled; +} + +static uint64_t ns_to_us(uint64_t ns) { + return ns / 1000ULL; +} + +static uint64_t ns_diff(uint64_t end, uint64_t start) { + return (end >= start) ? (end - start) : 0; +} + +static void maybe_log_latency_trace(const struct zvfs_req *req, const struct zvfs_resp *resp, + uint64_t client_start_ns, uint64_t client_send_done_ns, + uint64_t client_recv_ns, + uint64_t client_parse_done_ns) { + uint64_t total_ns; + uint64_t server_ns; + uint64_t residual_ns; + uint64_t client_to_server_ns; + uint64_t client_send_ns; + uint64_t server_rx_wait_ns; + uint64_t server_to_client_ns; + uint64_t resp_wait_ns; + uint64_t client_parse_ns; + uint64_t rx_to_dispatch_ns; + uint64_t dispatch_to_spdk_ns; + uint64_t spdk_ns; + uint64_t spdk_post_ns; + uint64_t wake_write_ns; + uint64_t wake_sched_ns; + uint64_t wake_to_tx_ns; + uint64_t reply_q_ns; + uint64_t cq_wait_ns; + + if (!latency_trace_enabled() || !req || !resp) { + return; + } + if (resp->status != 0 || (resp->trace.flags & ZVFS_RESP_TRACE_F_VALID) == 0) { + return; + } + if (req->opcode != ZVFS_OP_WRITE && req->opcode != ZVFS_OP_SYNC_MD) { + return; + } + + total_ns = ns_diff(client_parse_done_ns, client_start_ns); + server_ns = ns_diff(resp->trace.resp_tx_ns, resp->trace.req_rx_ns); + residual_ns = (total_ns >= server_ns) ? (total_ns - server_ns) : 0; + client_to_server_ns = ns_diff(resp->trace.req_rx_ns, client_start_ns); + client_send_ns = ns_diff(client_send_done_ns, client_start_ns); + server_rx_wait_ns = ns_diff(resp->trace.req_rx_ns, client_send_done_ns); + server_to_client_ns = ns_diff(client_parse_done_ns, resp->trace.resp_tx_ns); + resp_wait_ns = ns_diff(client_recv_ns, resp->trace.resp_tx_ns); + client_parse_ns = ns_diff(client_parse_done_ns, client_recv_ns); + rx_to_dispatch_ns = ns_diff(resp->trace.dispatch_ns, resp->trace.req_rx_ns); + dispatch_to_spdk_ns = ns_diff(resp->trace.spdk_start_ns, resp->trace.dispatch_ns); + spdk_ns = ns_diff(resp->trace.spdk_done_ns, resp->trace.spdk_start_ns); + spdk_post_ns = ns_diff(resp->trace.cq_push_ns, resp->trace.spdk_done_ns); + wake_write_ns = ns_diff(resp->trace.wake_write_ns, resp->trace.cq_push_ns); + wake_sched_ns = ns_diff(resp->trace.reactor_wake_ns, resp->trace.wake_write_ns); + wake_to_tx_ns = ns_diff(resp->trace.resp_tx_ns, resp->trace.reactor_wake_ns); + reply_q_ns = ns_diff(resp->trace.resp_tx_ns, resp->trace.spdk_done_ns); + cq_wait_ns = ns_diff(resp->trace.resp_tx_ns, resp->trace.cq_push_ns); + + if (req->opcode == ZVFS_OP_WRITE) { + uint64_t phase1_ns = 0; + uint64_t phase2_ns = spdk_ns; + + if ((resp->trace.flags & ZVFS_RESP_TRACE_F_PHASE1_VALID) != 0) { + phase1_ns = ns_diff(resp->trace.phase1_done_ns, resp->trace.spdk_start_ns); + phase2_ns = ns_diff(resp->trace.spdk_done_ns, resp->trace.phase1_done_ns); + } + + fprintf(stderr, + "[zvfs][trace][WRITE] total=%luus server=%luus residual=%luus " + "c2s=%luus send=%luus server_rx_wait=%luus " + "s2c=%luus resp_wait=%luus parse=%luus " + "rx_dispatch=%luus dispatch_spdk=%luus spdk=%luus " + "phase1=%luus phase2=%luus spdk_post=%luus " + "kick=%luus wake_sched=%luus wake_to_tx=%luus " + "reply_q=%luus cq_wait=%luus\n", + (unsigned long)ns_to_us(total_ns), + (unsigned long)ns_to_us(server_ns), + (unsigned long)ns_to_us(residual_ns), + (unsigned long)ns_to_us(client_to_server_ns), + (unsigned long)ns_to_us(client_send_ns), + (unsigned long)ns_to_us(server_rx_wait_ns), + (unsigned long)ns_to_us(server_to_client_ns), + (unsigned long)ns_to_us(resp_wait_ns), + (unsigned long)ns_to_us(client_parse_ns), + (unsigned long)ns_to_us(rx_to_dispatch_ns), + (unsigned long)ns_to_us(dispatch_to_spdk_ns), + (unsigned long)ns_to_us(spdk_ns), + (unsigned long)ns_to_us(phase1_ns), + (unsigned long)ns_to_us(phase2_ns), + (unsigned long)ns_to_us(spdk_post_ns), + (unsigned long)ns_to_us(wake_write_ns), + (unsigned long)ns_to_us(wake_sched_ns), + (unsigned long)ns_to_us(wake_to_tx_ns), + (unsigned long)ns_to_us(reply_q_ns), + (unsigned long)ns_to_us(cq_wait_ns)); + return; + } + + fprintf(stderr, + "[zvfs][trace][SYNC_MD] total=%luus server=%luus residual=%luus " + "c2s=%luus send=%luus server_rx_wait=%luus " + "s2c=%luus resp_wait=%luus parse=%luus " + "rx_dispatch=%luus dispatch_spdk=%luus spdk=%luus " + "spdk_post=%luus kick=%luus wake_sched=%luus wake_to_tx=%luus " + "reply_q=%luus cq_wait=%luus\n", + (unsigned long)ns_to_us(total_ns), + (unsigned long)ns_to_us(server_ns), + (unsigned long)ns_to_us(residual_ns), + (unsigned long)ns_to_us(client_to_server_ns), + (unsigned long)ns_to_us(client_send_ns), + (unsigned long)ns_to_us(server_rx_wait_ns), + (unsigned long)ns_to_us(server_to_client_ns), + (unsigned long)ns_to_us(resp_wait_ns), + (unsigned long)ns_to_us(client_parse_ns), + (unsigned long)ns_to_us(rx_to_dispatch_ns), + (unsigned long)ns_to_us(dispatch_to_spdk_ns), + (unsigned long)ns_to_us(spdk_ns), + (unsigned long)ns_to_us(spdk_post_ns), + (unsigned long)ns_to_us(wake_write_ns), + (unsigned long)ns_to_us(wake_sched_ns), + (unsigned long)ns_to_us(wake_to_tx_ns), + (unsigned long)ns_to_us(reply_q_ns), + (unsigned long)ns_to_us(cq_wait_ns)); +} + static const char *zvfs_ipc_socket_path(void) { const char *path = getenv("ZVFS_SOCKET_PATH"); if (path && path[0] != '\0') { @@ -119,6 +270,40 @@ static int write_all(int fd, const uint8_t *buf, size_t len) { return 0; } +static int writev_all(int fd, struct iovec *iov, int iovcnt) { + int idx = 0; + + while (idx < iovcnt) { + ssize_t n = writev(fd, &iov[idx], iovcnt - idx); + + if (n > 0) { + size_t remaining = (size_t)n; + + while (idx < iovcnt && remaining >= iov[idx].iov_len) { + remaining -= iov[idx].iov_len; + idx++; + } + + if (idx < iovcnt && remaining > 0) { + iov[idx].iov_base = (uint8_t *)iov[idx].iov_base + remaining; + iov[idx].iov_len -= remaining; + } + continue; + } + + if (n < 0 && errno == EINTR) { + continue; + } + + if (n == 0) { + errno = EPIPE; + } + return -1; + } + + return 0; +} + static int try_pop_resp(struct ipc_client_ctx *ctx, struct zvfs_resp *resp) { size_t consumed = zvfs_deserialize_resp(ctx->rx_buf, ctx->rx_len, resp); if (consumed == 0) { @@ -206,6 +391,10 @@ static int set_errno_by_status(int status) { static int ipc_do_req(struct zvfs_req *req, struct zvfs_resp *resp_out) { struct ipc_client_ctx *ctx = &g_ipc_tls; + uint64_t client_start_ns; + uint64_t client_send_done_ns; + uint64_t client_recv_ns; + uint64_t client_done_ns; if (ipc_ensure_buffers(ctx) != 0) { return -1; @@ -215,22 +404,70 @@ static int ipc_do_req(struct zvfs_req *req, struct zvfs_resp *resp_out) { return -1; } - size_t tx_len = zvfs_serialize_req(req, ctx->tx_buf, ZVFS_IPC_BUF_SIZE); - if (tx_len == 0) { - errno = EMSGSIZE; - return -1; + client_start_ns = now_mono_ns(); + + if (req->opcode == ZVFS_OP_WRITE) { + struct zvfs_req_header header = { + .opcode = req->opcode, + .payload_len = (uint32_t)(ZVFS_REQ_WRITE_FIXED_WIRE_SIZE + req->length), + }; + struct zvfs_req_write_body body = { + .handle_id = req->handle_id, + .offset = req->offset, + .length = req->length, + .flags = req->write_flags, + .data = req->data, + }; + uint8_t hdr_buf[ZVFS_REQ_HEADER_WIRE_SIZE]; + uint8_t meta_buf[ZVFS_REQ_WRITE_FIXED_WIRE_SIZE]; + struct iovec iov[3]; + + if (req->length > UINT32_MAX) { + errno = EMSGSIZE; + return -1; + } + if (zvfs_serialize_req_header(&header, hdr_buf, sizeof(hdr_buf)) != sizeof(hdr_buf) || + zvfs_serialize_req_write_fixed(&body, meta_buf, sizeof(meta_buf)) != sizeof(meta_buf)) { + errno = EMSGSIZE; + return -1; + } + + iov[0].iov_base = hdr_buf; + iov[0].iov_len = sizeof(hdr_buf); + iov[1].iov_base = meta_buf; + iov[1].iov_len = sizeof(meta_buf); + iov[2].iov_base = req->data; + iov[2].iov_len = (size_t)req->length; + + if (writev_all(ctx->fd, iov, 3) != 0) { + ipc_close_conn(ctx); + return -1; + } + } else { + size_t tx_len = zvfs_serialize_req(req, ctx->tx_buf, ZVFS_IPC_BUF_SIZE); + if (tx_len == 0) { + errno = EMSGSIZE; + return -1; + } + + if (write_all(ctx->fd, ctx->tx_buf, tx_len) != 0) { + ipc_close_conn(ctx); + return -1; + } } - if (write_all(ctx->fd, ctx->tx_buf, tx_len) != 0) { - ipc_close_conn(ctx); - return -1; - } + client_send_done_ns = now_mono_ns(); if (recv_one_resp(ctx, resp_out) != 0) { ipc_close_conn(ctx); return -1; } + client_recv_ns = now_mono_ns(); + client_done_ns = now_mono_ns(); + maybe_log_latency_trace(req, resp_out, client_start_ns, client_send_done_ns, + client_recv_ns, client_done_ns); + return set_errno_by_status(resp_out->status); } diff --git a/zvfs架构图.excalidraw.svg b/zvfs架构图.excalidraw.svg index f6fa23c..12ac9ce 100644 --- a/zvfs架构图.excalidraw.svg +++ b/zvfs架构图.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19yW4ra5Levp5CuL2pclx1MDAxN7P/eSh0NSCJmiVqXHUwMDE2Jfk0XHUwMDA0ipNIUSTFQVOj1r3yyrA3NuA38MaAN4V+m3bXaziCOlx1MDAxMpPMP5OZVJLixSXr1lx1MDAxOUQyT1x1MDAwZTF8MX3xL79bWfml99ou//KnlV/KL8VCo1bqXHUwMDE0nn/5I/78qdzp1lpNeItccv7ebfU7xcEn73q9dvdP//BcdTAwMGb4WY88tWrFsldsPbx/rdwoP5SbvS588D/D31dW/mXwK7xTK+GX355uWm9vx0KUr8rV9ctTftGlucFXXHUwMDA3XHUwMDFm+jibTrnYKzSrjfLwrVx1MDAxN/i55NyThlx1MDAwZl+f777Cu1x1MDAxOa0+//5cXCv17uBnSilPfv70rlxcq9714MeUSe1RyoUwhjDJxfAz7//sn1bI50+6vU7rvrzearQ6eG5/R1x1MDAwNq/hmd1cdTAwMTaK99VOq98sfX6m1yk0u+1CXHUwMDA3bsXwc5Vao3Hae22838VC8a7f8V3f+7+S/3nedOznn9/rtuApXHK/XHUwMDA1/2z1rlnudke+02pcdTAwMTeKtVx1MDAxZd5cdTAwMTNKhteB59jeKVxyns0/+4/QLP08wsfDXHUwMDFhPlx0/vMnf1x1MDAxOZ5PuYxPklqhqdHc2M93hlwiY1xyXHUwMDFk/2mu1Vx1MDAxY4iP0ZZbS8jwhte6WZCa3uColUKjW1x1MDAxZd5YPLWNcYnyS5VPsspHt7m7dfnAz1itvU7l6f5e6/7zRo1IV6HTaT3/8vnOX/5cdTAwMTh13Ge2V613XG73j3me31xcN69cdTAwMDfruVothePmr1x1MDAwZU6fy4e957Nda5S5vCm/nq+ncNzXYuO8/pi52sxcdTAwMWORyt7GXHUwMDE5I3d5kcJxXHUwMDFi11x1MDAwZnVVeDk1rYssb1QuKm1T76Vw3MruXeksV97WZ6dr1dfy4/Vep0pTOG6LXHUwMDFkvFCre8TWz2XrcufqtG/SuFx1MDAwZnc3uvpy99Beq1/kXHUwMDFipfvn+sFL9TGF4+p8pdfcJ437+8PsXHUwMDE2P7vsXHUwMDFk5V5ZXG7HJey19vJ23dmt1U9q2/uku3FOeVxux1WZ9Y3nm2L2VL8085Wy3b2Rt9cpXHUwMDFj9+KkfrRxXFypk1Jtj1x1MDAxNreaJ6+C6XjH/fmnoW3rt0uFd8NCtebMcviN+Wx9o9a8hzeb/UZj+LNW8X5oi37nO+ExV2Yyj3aj+vZwrdpnj3t9oqtdn8GZ5Mq04lx1MDAxZeehrox7eniaXHUwMDFmzoxahytTw28uXZf/zoe6Lqml4OC+hjdz6LlUuOdSRGrG2PD9LzuuXHUwMDBmXHUwMDEx6ZVfxizpu4SV+quFt9zb0+H97fZJe1+KXdKoxtWjrZvXtYeNg6rMV8xDM/+2W7JXpTT8wH25cqb1baauy43HM1Ffvzw+TUk/mSRG+FThS/rpvntcdTAwMDH9XHUwMDFjuflcdTAwMDPVNER5JlQ1qfKYQzWJXHSqJluqZnf88X3AR1x1MDAwMcpElVxmaNtAP0mYXHUwMDBlUiqV4ML6jF58LVx1MDAxYzmPr4veUJJQguAmnlx1MDAxY62vnJY7cMq+p9Nq9k5rb3jujIz8dLPwUGu8fsRXn1x1MDAwN1pt1Kp4XHUwMDEzfinCWfuPXHUwMDA0h+3VIEb7/MBDrVTye5XbQrdcZmc9uE1DYSzCP1WAn3Z24jitVqdWrTVcdTAwMGKNs+AlRWha7mAvl9tv5sXZ89tbLbOX7fZvT+Jomlx1MDAxNDLKXHRKXHUwMDFi0DO+1LNkeqZcdTAwMTVRhHDtUjRcdTAwMWF0gZ+Kxlx1MDAwMClR0Ipp3N2sXHUwMDE1LVsuPLSavz/q1Fx1MDAxZVxundeVh3KvXHUwMDAw/0ZhpfVcZnL+hzSUr1Gu9FwiVK/XaifTu5GrXHUwMDFiV7JcdJdcdTAwMTOheHv3j43+wfld5y1X2WpcXD+93Vxc6+f4XHUwMDEw1DBPcVx1MDAwM+G4XHUwMDAyqyp9kTzeXHUwMDE5y7RHXHUwMDE050pcboXpXHUwMDEyXHUwMDFl0EWqbVBcdTAwMTcpW6ZSXHUwMDEy4lEjXHUwMDE44Vx1MDAxMKq6NFx1MDAxNGKWUFx1MDAxNeWaXHUwMDE5UFObYiplXHUwMDEyXCK9vnvi+ZuaPrhZXbuVXHUwMDA3++drr6xcdTAwMTlcdTAwMTc5smarvL29Wd3avSy3svRk5ynb76ZcdTAwMTXZMSpcdTAwMDFcdTAwMTekg1x1MDAxY91XXHUwMDE5XHUwMDA3OVLh0XCN4mySRlHl0Ci61Khw91x1MDAwNvdZXGLjxJHWXHUwMDA2cpNcdTAwMWa6o1x1MDAxOSGGwWNI27sll8OAd7tttG5v7lxuTVx1MDAwMHd/+lx1MDAwMVx1MDAwZqp0f4M/+k8/moN3aqVcdTAwMWbNTrlyU4TT6v1oep737WiTXHUwMDBl/60xuDnBQY17wuSX7tTiXGK/5zOtQrAw8aDGXHUwMDAyZIpcdTAwMTnsv9uMTn2XXHUwMDFmbrw+7m/t5DdzR/dcdTAwMDc7OXG4OOpcdTAwMTU0XHUwMDFj71xigEfYK22kJ532Ki2j82n3jMPuqaFD/EjccFxyQNbvKdPxwyk5x83T43z92PRKmVx1MDAxZFI/PF57K1+qw9jlgNzrWnO1v83ub5rX5F5vvPWfX1NI19Tpk71rtVu5pzt1fHFW3H7pPFx1MDAxNVM47q/Ambv9tM9cdTAwMDJcdTAwMTBtQi2AYFZcdTAwMDNcdTAwMDRmcVx1MDAxMlxyUY9/wS2A4TpcIlx1MDAwNjBEz8lcdTAwMDJoXHUwMDFkNFx1MDAwML57/9NcdTAwMDAwwLxSWIhcdTAwMWanxVxy83LmiT3xu1Sg5/M/YHds+TXPPeaiJziucVx1MDAxN/15jlx1MDAxMfj5VO7s5baf7NvmXHUwMDExPzrObLCb7bOzRJVcdTAwMTFiKVPwnCnThI6IJIRKXHUwMDFlt8ow+f6rMVx1MDAwMVmiVji8yVx1MDAxMkQnrfAzQ+EpaOGq8EtcdTAwMTZcdTAwMGWeuFx1MDAwMEdNpe97M49LXHUwMDFmXHUwMDBi+/RU8uPtUlnX8i/PtaqtPsR1Zbnjje725dnRXeNxrbJdZKtX+2tbKbhIWe+UXHUwMDBiW/eba7Zx1lx1MDAxM9vrZ7dHmXZcbsfdz3VPXHUwMDBiXHUwMDA3Lzx3dn9l1d7aVbnT7KTmepmRLKU42v1UYsTRcCqeXGY3XHUwMDAxTEw0XHUwMDAxPle1TFx1MDAxM0+Oo1x1MDAxOVx1MDAwM1xcraizmUfQ0JIoNlx1MDAwMEmq/Fx1MDAwZmhxssSD8LF3XHUwMDA3Oli6XHUwMDE53JVvj5NDqzJcdTAwMTNcdTAwMWPmuFx1MDAwZlx1MDAwZV5ZhFx1MDAxMu5cdTAwMTW0Lj10xEa93ezv9CqHm50zncBcdTAwMTmDrokwTaREemZcbmcs1VJcdTAwMTOTOWNOqWaaXHUwMDA3ksFcdTAwMDOxXHUwMDEyofrJrWbSSqqn0M8pXfF99VTubper13Qvs5uXa92LzjqbQ3PBrFxc/ESXmWrTgvvuxXCZhlx1MDAwYs+EKypjk1xcplxceswkXHUwMDFlU2hG/PlBnz7KcH0kVjLK/NXYxfGXR+CpXHUwMDE2uHNhglx1MDAxZlx1MDAxYveR71dcdTAwMTOhadGmZsxcdTAwMWOMqpqwnlx1MDAxMJZIo1x1MDAxOFVcXIhcdTAwMTFVU2TUJbr6+DzAtkRoTVxy41JcdTAwMWJOg6onRjpcdTAwMDB/y9pcdTAwMTf0kizMS1LBKVx1MDAxMcGSabSXZIJbKoiexknOSinbrdq4XHUwMDE3XHUwMDFl/mllKFx1MDAwN4O/fP75n//o/HS4vOFrIGnDQ1x1MDAwNPxdo9DtrbdcdTAwMWVcdTAwMWVqPbi2IzyvgEnsXHUwMDE1Or21WrNUa1ZHXHUwMDFm2M9xkDi9R1x1MDAwM+NS7OM1XHUwMDEzjzKrjaaSaKE44dL3oWqhPVAhJUaULCBcdTAwMTLlZmnyKUVcdTAwMWJcdTAwMTXfKWWIR4yBmNRoI5SF50u046RY8DxcdTAwMDY3Z1x1MDAxNa3IXHUwMDFk4PXxW1x1MDAwN2fpf8+PPkJ6rVwikUyk2ZJcdTAwMDDWlbTvJWhORs1cdTAwMTbFfmRcdTAwMTZccuU9XGL3LKFcdTAwMTZcdTAwMTAlgUPIof3/NFtcXHqS2uFreJSlXHTzP6BQXHUwMDEzXHUwMDA2OspcdTAwMDGzXHUwMDEzVyTOhFx1MDAxYf/psDHSXHUwMDEwLZlZqH6tlI1YmPThKyB38zBosa1cdTAwMDdcdTAwMThcdTAwMGZNuVJgqDjcSzAj3PepXHUwMDBm4yE48b2UXHSISCyTXHUwMDE2nUtcdTAwMTgzaURcdTAwMTBGQJYlYZKCoVxynJXwpPSdXHUwMDE0pbOzb7Gq9sqEtyxaTTinOlZv8M+Zm8P9+u3B8+u13ttrXHUwMDFm3jw8XHUwMDFmnq+rxTEs7pKdMuBcdTAwMWaNUVYxXHUwMDAwodxnXHUwMDEz8PtCQlx1MDAxY8hBzrRcdTAwMTVEgquaYdWeufr/fF1cdTAwMTVcdTAwMWaoXGa8uGDaP3CxUHX7Tun26bq7dVwi107Orjqnur5fyuvvTvJ/oVx1MDAxZiBg8Fx1MDAxY0l+ocjM6+vShuoqI1xcXHUwMDFia5hN0GDjfEqLrqtWROiqpMLjfl2dnapcdTAwMTJHiFx1MDAxOayvg3M1cFx1MDAxZZIl0dQv1Ne5oCBcdTAwMDTTQIup6utcdTAwMGallfesedd/J2deYZ/gZcYzXHUwMDE3vrOc3k/qYMP/MCmsrGbxajbvqrf6tFFsXHUwMDE2Svu0dKbu781htrrRvlp01Vx1MDAwM3VcdTAwMDPESISlXHUwMDE0/shGR0uE5IvmJVx1MDAxObPGMuUzXHUwMDAyXHUwMDBi5STZ0/VcdTAwMTnJrVx1MDAxNrr26am/3snt39CT3nc3oc20XHUwMDEyXHUwMDBlSF34+rFn5CSVXHSN6bggSlxixePkpaJcdTAwMWXSoisqM+GKXG5RyZxcXKRrmMXhIcGTXHUwMDEyqoiZU1x1MDAwN1x1MDAxYVx1MDAxN1JcdJJAXGK/5iFrrZ++Z65cdTAwMGVygn9cdTAwMTl3kMOTdGpcXFx1MDAxY1xcXHUwMDFlmSfT3CNKU1wiIJhk/trp4J5p7VFf6jE4olxiXHUwMDFmXHUwMDEwXHUwMDE2XHUwMDEwXHUwMDFmYVx1MDAwMufulGukQytPWWopk4JrRsmyXHUwMDFlnjTTb5Q0VPtccoIvT6ZCXHUwMDA3iKWZ1dxH0pDq88epZskyodKHr6DcXHKPXHUwMDE3cIippcmSZKSYXHUwMDA2zbNWKlx1MDAwMy9iXHUwMDFkSXbjTZvsj8bhK/50XHUwMDFkI9Qwylx1MDAxOINfXHTgZVx1MDAxMziNXHUwMDE5JsLiYJtoXHUwMDAzJjxcIiQx2kqrXHUwMDAx5o5cdTAwMTkw6SFcclxumDgtXHJXJFifxLlcdTAwMGZcdTAwMDGyXHUwMDAz71x1MDAxYfg6d1ow6Vxyhqg4I0xcdTAwMTJOhr5kacH8XHUwMDBmKDzRXHUwMDBmT4Bpa1xcY58sOFxmOkz0a8pAw9NP9L9cdTAwMDPe4T/8XSYsVPrwXHUwMDE1lLuFsmDEwzIr52BjLVx1MDAwMVx1MDAxYqyVXGbWLpWnXHLhhlx1MDAwMqZFMDllnj9cdTAwMWE0jVozLTScXGZV0nIwqkFjxjwrqFx1MDAxMNoqxog2Kiiz6dq29UZNKd1j/Vx1MDAxMi9sX673Xjpbr8dxupxcdTAwMTSVnuChuW+F3CyMa22sosRh2eRyvjZZXzBhXHUwMDAwJlx1MDAwMOs6XHUwMDFinUz4XGZcdTAwMDBopjRWxynFzFx1MDAwYmd9xDhFXGJcZnrlXHUwMDFmzdLgfH40W+1y80ez2Gh1yzhe2sWYafooKn5QXHUwMDE0SlwiXHUwMDExOk5cdTAwMWLJXCJcdTAwMTHnslwiVPKs0VGsXHUwMDBit3qPPT6/Pb+ubtX6h3FUXHUwMDEy4iQ9opOjcEOAwVx1MDAwZcm0fSilcMx+2SWeXGLTSc64MdqJXHUwMDFiqFxiZ4ugRktcdOB/XHUwMDFhtohZqyRcdTAwMDbzP5rPnVpvIKuVv//7wW+ZzLcqou9cdTAwMTYn0MPwa4nQvuhqX1x1MDAxNNjXkntKYFx1MDAwN5thnMDDXHUwMDE40T5FKPYtOdJnn+lpLlx1MDAwMG5RVGCKXVx1MDAwN8zFlmuYh71cdFxmXHUwMDAwXCJ8Si9n6ZKCfVx1MDAwZY9cYm6w0436k8xBXHUwMDFlXHUwMDAyLjVY1WlG6X5cdTAwMTX5inDxXHUwMDFivFx1MDAxYlx1MDAxMLx5oP1cdTAwMDR5XHUwMDAybuFcdMHDtczAQ7K+UOWzf8bRXHUwMDEyXHUwMDE4XHUwMDBi30fP5Y6eXHUwMDA1k9j1XHUwMDBmMaGEoMMwXHUwMDExOFxy5mGujPDBr1x1MDAxMHWKWSP86IpPlEGz8MwhvmWWgZ9cdTAwMDNcdTAwMTg/XG4nYlx1MDAxODSqPVx1MDAwZd5cdTAwMGVuimJcdTAwMDZuilx1MDAwYvKPyZVcXM45JM2/Srh3klnpgiGChMJcdTAwMTBG4PlcYitk+lx1MDAxNm1B0lx1MDAxN5lwXHUwMDAxxFdQ9OZh0mInXHUwMDBiMsSTcDfh/0ZrXHUwMDFj7jTBnkA5e5uGmWBcdTAwMGK6XHUwMDBl8b5cdTAwMDTDKrRw5FHmbtSiOU+iU7LKs1JBUIyFXHUwMDBi/7qBXHUwMDAxzYZcdTAwMTRIwqFcdTAwMTVjRikpgnlcdTAwMGKcKdHSXHUwMDE4cDPgcuBcdTAwMTiOdlx1MDAxZVx1MDAxZFVoXVx1MDAxYTGHXHUwMDExgyeBpVx1MDAwNuWyYY7FXHUwMDA2Q1x1MDAxYoY8JJSZXHUwMDE5ZDeotIl6XHUwMDBlZmTDwuVccl/ad4DZ2awkxoJQrrjSXHUwMDFhkCRcIlx1MDAwN0tdOU40tj8nszD5Pp1cdItmNFx1MDAxYoFlXHUwMDA029uE0ETBaY3Q5/hOSrC5mrBmodh7vbl8fanzarV7s/1aPN/YXHUwMDBimrBQxlCNczeAeKmCkFOOlsYpRW5cdTAwMTYztGPBdSwgSZ4rwlxcRpRJXHUwMDA3wsFcYlx1MDAxOaFDWENVaPNcdTAwMWbXXHUwMDFhW2/nyWK/n33dvNDPXHUwMDBmXHUwMDE1tWpPXHUwMDFl2yZXb3ZysWnGzmkuX99+vT9bz26Tg8tcdTAwMWM76NRTa4OmksskzTVcdTAwMTGK5b7KgGI5XHUwMDA2tyXzmKahOoVkJz6dXHUwMDEyXHUwMDAxnZLR1Fm/ZX1ypU9ccvhcdFx1MDAwMt7dWdFgoUOiymhBVKx+9eRcdTAwMWNjiYRwKFM/M465i4f5VCymmtue4HHG06eDi4nC4JFcdTAwMDZhUl+XsaHOi0hcdTAwMDA9kYpGna5cdTAwMGLUd+m8XHUwMDEyXHUwMDAy78HqMEA7SZu3IGJGnqJcdTAwMTmUMFx1MDAxMHd/f+7AL2H4QtlKXHUwMDEzao9jx8lwN1p7x0C4pEpcdFxcpyeV1L5NOitzaJqKx/lrSWiunXHNXHUwMDA0Nyx+t7XcOrl6y2aq2atruXet8jqv91x1MDAxN57wM1x1MDAwM9GcJznXnFBcdTAwMWNHXHUwMDFkMYJcdTAwMTljPCrEXHUwMDA3j1xmmeFUXHUwMDA0JdbzT5Y6ZskpwFwiT0pqMWCjXHUwMDEwRlx1MDAwNbqxXHKIXHUwMDFhxFbkm1x1MDAwNlx1MDAwYme7IHBWi/xmtXDvXHUwMDBii/EmQnVmwLjQlKA6aMDpavlp9+mAdLY5XHUwMDEzudbdVixawoy2xrPE/NRcdTAwMWR/l95Ae5TxXGK1n9rjWyX2IfaML9fXJFx1MDAwMevKciO43+P7gIJcdTAwMGWvm0KEy8BypI9cdTAwMTRS4Fk67N2VOytHnVZcdTAwMTGu+/enZcDLpULnNZW9NdM3XHUwMDFkTbW5JvRSXCK0b5tuXp/s7lx1MDAxZbX3rnb16nk2qy5fXHUwMDFkjUYhXHUwMDE5KHBf4Fx1MDAxM4aJPDXaXHUwMDAxqEdoYfzdXHUwMDEww0lY61BBX6vHb1lcdTAwMDWTsFx1MDAwM3NrrVx1MDAxY5ng9iXPRXj/slG4+Y3oOW6tye5dvlxcPDxcdTAwMTTrh89XW6vHpVxcRV83Yu9RPO2vPz7Itbte7VmXSnuH9Z11mYK7zO1d73ZcdTAwMWG99Z3V+21ReH5hq+u7aewrLp7sVFx1MDAxYeeZyqXOlFx1MDAwYmell07muHKYXHUwMDA2XHUwMDFj2aOrp5XVx1x1MDAxN1F4qdyzm63sebmfwnGzeVq4vq8+bfc65d11XT9tr2WvUjjuJbnaKbdOaLPztrPfu8o8XFzd3lx1MDAxZaRw3HudzWb2tmpHveNMpp2TtcvMeiWF467aXvFss19qrr1cdTAwMTZaO7eb2eKLSON89era+lOvf9vvyEIn02jmb64qMfdcbk+EZeCZIWZKXHUwMDEyN0c4XHUwMDA2t5bGgWWc8VGfMEptxe2YV1xiOlx1MDAwNeZcYj+WsCw8h6q11cI/q+ufXVx0p79UmlhGjVlEWHZcdTAwMDCY51x1MDAwM8osbjZ1XHUwMDAyelx1MDAxYVx1MDAwN2gjXHUwMDE3XHUwMDE1oXtXh418ub+zSyrtZ1x1MDAwMHBP4q1759iVXHUwMDFiXHUwMDBlyqRcdTAwMDdhulx1MDAxMJRg2MNHQVx1MDAxOSBxT2slXHUwMDA2RCjSXHUwMDA3XHJ8yYDBJkJtXGJ8iCpXkEQ5XVx1MDAxMmImR2lcdTAwMTA1XHUwMDBiNjLwOtRUI0PX3ygjtE11g8NsQc9Dd61xTLiix2zjcLd72qu+mDScZ72dOTja2Hqm5G5j+2J3TfHN17c0wNTR5f7T2k6/U6yQ7truWiZ7eVx1MDAxNJO4XCLyuFx1MDAxN1ltXsonVuu1XHUwMDE3nrlsZK5e6+dcdTAwMGJcZk5cdTAwMWVbh71b2Xo7otnuOl873XwjtprCcWV252qjvr5zfvV4d777eHB6mbuMmYv6XHUwMDE2cD0rXHUwMDEwvJ/dXHUwMDA1n3e0nj3q0+uz9uZRs5I7WmBwXW1vnNC9x3pP71bYdVaLjfxqTLD6LaC99GpE7eKu2mdcdTAwMGbHsmMutu+veFx1MDAxYfe3t1FcdTAwMDWnelKUmzvPSlx1MDAxZeS3ZIXm01x1MDAwM+2WpbXipV/Pme5Z+fLqZOdsv360frl3sJeJXHUwMDA12lx1MDAwMUF6TFx1MDAwM5KEXHUwMDAwXHUwMDAy27ZGN48xTTykL5DKKqGNXGYmcnxlwiVkj7Ep1Vx1MDAxMkBXJoSyPnySUylLlFwiNvV+7XfILlx1MDAxMohhXHUwMDAwsm9cdTAwMWZcdTAwMWXuzVx1MDAwNaqnmzVcdTAwMWScdoRORWecolpcdTAwMWMyXHUwMDFhUDInRjFCNMDxsXV+XGKzXHUwMDA15sYpM8JcdTAwMWGmglBcXHiESOS90MxcdTAwMWHQPkfDg7XKs0IykFx0pjUhS51L2P2A/MTIzuFe+Fx1MDAxZLGVVnKwl7igKz1YPil1mnnhpdpzZm3vuXe83d6vPT9ty93k21GmUvNUWytCXHUwMDA1XHUwMDFiX1x1MDAwMZFcdTAwMWVcdTAwMWUtcIGp9TdHh/Ir/k5iI7mxXHUwMDEwkGE7tvRxea58NFZoXGKwXHTm41xyhOJK82BAONLtXHUwMDExPN30OozdXHUwMDEyXHUwMDEzXHUwMDBiXHUwMDExKOtcdTAwMTmiXGLYL7hcXClHhySksVx1MDAxZbdEMMWEMZxcdTAwMDX56X311iVcIojD7Vx1MDAwMKJCRyjM/IggNDcgXHUwMDE0rn2jfFx1MDAwNjOpXG7h4FdcdTAwMTDB3/7H//5///bf/uO//pd//7f/ubhJvFx0XHUwMDBlflx1MDAxYy+MXFxUhObpjUL9uH1fJy9wXHUwMDE2TXV3vdrdLFx1MDAwNzUvPInHPKybg31cdTAwMDTh4mQ0iYd1U49zJC1C1E5cdTAwMWPtkcS6OvuXldWkOTtMgVx1MDAwMlRzVlZlOF0olVx1MDAxMErh6tVpxpKmhFx1MDAwN7s1enBgq5ulXHUwMDBi0VCX4qljrlxuV9+dtJtVXHUwMDEy7NeWrJpVMuU3n/RwS31cdTAwMWOIXHUwMDAzZzJiY4PTXHUwMDFlZIKRdbFJL0FOeNqDMKw5Me6cU49oU+FMMlx1MDAwYs9nXHUwMDEx81x1MDAxZfu1Zv/lP/77v/77X//v3/7PX//21/+1uFBnXHUwMDAyJlx1MDAxOYc6wSuLUMPoYlJknkRcdTAwMTHuobNUTFNcYvzGQo33OUZcIlx1MDAxNKfCSFTWgFx1MDAxZVx1MDAxMk9aQrRcdTAwMDXESrjhxknyLqknhWKKXHUwMDAwWMbqpliqqVx1MDAxM1xuhbNMcGqIwa1PLihEQ9eLUFxiXHUwMDEyIVTWcpqVflNCoftK+7Jzn+nuZs9zotfd3yue9Sq/xkxJJlxctvFcdTAwMTWU6uFcdTAwMDFcdTAwMDPX+Fx1MDAxZMlcdTAwMTLFXGLHWFx1MDAwNC5A+EaMVnxcdTAwMDTCnCuutVx1MDAwNExcdTAwMDA+dspB8OiWiZWRyVx1MDAxOFx1MDAxY1uSimD+jklcdTAwMTU8pZmTXHS7hTNcdTAwMGViUZx5XGa5OJDNxOoxxk2qtKckx05bJExWrr3Cy/HURICFg2SCXrnrNOHzqYxcdTAwMTiqXHUwMDE1suamXHJY3tMyQ1s7NeXm4qKUXHRIwk21XHUwMDE5uX0gMqSbXHUwMDAwTayHzFx1MDAxYlZQjm2Lo1x1MDAxM1rgXHUwMDBiPWu5spRoQnDSLlxiTYL6ppE1izAqXHTjwjK+XFw3kFx1MDAxMIgwXCKENOBcdTAwMTPdNZuIpFxm3HNcdTAwMGJcdTAwMTDGl5ueOVx1MDAxMnl4LFxc7l5d5io98va4WsxcdTAwMTSOns/lr1x1MDAxMYmMfDogw/OAXHUwMDFksZ08wlx1MDAwZZxQRClBbkVcdTAwMWSs0UDEoTTTjOG6TSWlmlx1MDAxMnhEh1RjwINccnoukTFcdTAwMTZrJ8FtXG7KXHUwMDAzLFx1MDAwNKdkNddMacfS0ZSRiFs4YyFcdTAwMTFhkPovxDDC38EwXG6tsDpcdTAwMDGhQbDJWzuAyJL7O2L2TktJtVx0LlxcRVBcdTAwMTc6o28p01x1MDAxNleZpVxyQ0A6ccPv12HIj+ZLodfr/PlH87bRuq2VfjQrpX/8p1x1MDAxZs27Qlx1MDAxM5CD/1x1MDAxZX9cdTAwMTNMXHTl/55cdTAwMDQrwijBJ1xcb4S2Rvc7RuJcdTAwMThJhKdcdTAwMTRcdTAwMTbTkY+fyPFyXHUwMDEy9ZiAKJNDZMEgXo+FYyCI8lx1MDAwNqxHXHUwMDEyvFx1MDAwMFx1MDAwNCNL5U2IY7CRwFx1MDAxMOJsXHUwMDA3l8GGlCGKsVx1MDAxMOcj5pxcdTAwMWaKyTRcdTAwMWWyzy+lzEHtUNVli73Jbnv1V49iXHUwMDAyXHUwMDEyPFx1MDAwZlx1MDAxNJMgeVwiXHUwMDA0seBKqbbITuroNFFcdTAwMWVcdTAwMDb7wjBcYj5cZuBcdTAwMTk7XHUwMDFkiEmSPVx1MDAxMVx1MDAxY0w/2FxuJVx1MDAxOaHasVx1MDAxMmrm6Vx1MDAxM7csxlx1MDAwMS2SaY+GW0GQXHUwMDA2hcSFlEuI6WyQzVg4SupiXHUwMDE5voVnT1x1MDAxNMH/iDNOXHUwMDBin3ehUlBJhVx1MDAxMqlnT5hcdTAwMTZW+3qVp4At75s8Kt9cdTAwMGVMxPCZjeGSXHQwYVx1MDAxY5d8XlBUnFx1MDAxMNlcdTAwMWNcdTAwMTCNPKjwqGTIpCqMtWZU58BcdTAwMDF6kkVMo1x1MDAxMc9Q0FSipdGUcE1cdTAwMWMqqFx1MDAwMdxw0GbwL8RSX0r3N62RSSo7mKNcdTAwMDdcdTAwMGbjjC6Q6T9UU1x1MDAwNVx1MDAwN+2Wis+xXHT2uH9Yfz1aJVx1MDAwN6uX5ZY1Zb3evTn7VUKRUNHGV0Co54FNXHUwMDEyZFiEXHUwMDFjrLTkYrCV0UcvXHUwMDFiXHUwMDAxXHUwMDA0Us+oXGLJrDBKwk1cdTAwMTJGXHUwMDFhXHUwMDEzRCPSXHUwMDBisqOnjEfcXHUwMDAyXHUwMDE5XHUwMDBijyDbqOBSKYaTNXy08K0sxGxcdTAwMTC+XHUwMDBiXCI1Mlx1MDAxNlx1MDAwN4nIXUmU5a6mcDyiXHUwMDA3W5ecXGbjTIVcdTAwMGbKa1x1MDAwM9+STKQ/KG9cdTAwMDCP+HDQXHUwMDE34Mii5U1C1zVNwlx1MDAxMmHwJEmiJJpcdTAwMWQlXHUwMDEyrnBrPTC4XHUwMDEwYVx1MDAwM1TRQvHxZlx1MDAxNFx0XHUwMDExo8RpbFBbpoI6STyiXHUwMDE5jt9zXFw6wVx1MDAxOFx1MDAwNEtBJaWSeFx1MDAxMNYh6sG2XHUwMDE2vaxcdTAwMDElhixcdTAwMDLuLoSh1NUvz4NccmafXHUwMDE5UXiwYGin6pefXHUwMDEysJxuPIr+/mXppreZzYpcdTAwMDd5e/xoSolcdTAwMDCLgkDbTlx1MDAxNbWkXGZYQkVcdTAwMWJfQaGeXHUwMDA3ZEmQTmG4XHUwMDE5iiqw5oQyqoOLmSj1qLWU2ffEXHUwMDFj4NpoXGIzy8lcdTAwMWS31MRBXHUwMDE1gnHPaLhcdTAwMDLKNDH+ZdDvWVx1MDAwZeNcdTAwMTFcdTAwMDLoXHJz+FxmXHUwMDE0KJjrXHUwMDE1jva5ZZojXHUwMDE0ViDxXHUwMDFhXHUwMDA0sC5DZMMjJ6ZcdTAwMDdcdTAwMGIy0ucwXHUwMDA3teRaTEWf/Fx1MDAwMSpcdTAwMDb7XHUwMDEyV1x1MDAxNjvLMcHHj8NcYt8lRShd9LDCROBcdTAwMDBcdTAwMTjeKKQ1XHUwMDE2yoxN7GCrXGLjylgkNIdYxaF2Ia1cIlx1MDAxMFx1MDAxZVphXHQ1Ylx07Vx1MDAxM69117j4hDs7VnV4+5bVilx1MDAxMohcdTAwMTPmmNXotcvXvZ7uiduTrfzebmfnLtdcIslAXHUwMDAyKD6dKpaYbZuIX37nXHUwMDAxXGJcdTAwMTLkMFx1MDAxOOipYIPNhZZxQlxcq1x1MDAxYTWuVOaCXCL5LZU6KGqpJzUg0MRFWFpcdTAwMTKwJUaxXHUwMDA16Fx1MDAxM3FLZywwXCKFJ8LNXCKgRY9SxUHZrNScOilil2gkXHUwMDAxXHUwMDFhXHUwMDAxYZaaaO0yeTa8R99cdTAwMTJcdHEtmyqRO6FTXHUwMDA0MPd0wctcdTAwMDdcdTAwMWOp9Dr9Zlx1MDAxMY76+0rpXHUwMDBmg/Xpb7Vv719cckcmXHUwMDEzQMQ4MnFfXYQyRtN/RYFcdTAwMTRhPFx1MDAxY6RiXHUwMDEyVI4yQ0aZOZGQhIA1NJRQajlx8DU7u0CoJy1cdTAwMWWQXHUwMDE4+Fx1MDAxZuXLbvKkXGYkmGeCmNm5fyVihzxcdTAwMTh/8Fx1MDAxMNOp7JQo5eAm166e9O/Vhu3c8lxy0d8s3yVCKeBkteBcdTAwMGKGUoJcIjxcdTAwMGacXHUwMDEyO3GBI0BcbodWwHlcdTAwMDJcdTAwMDRcdTAwMDFvXHUwMDE5XFxdqD1cdTAwMGXiIKWC91x07u+eXHUwMDBlpiTATnhKuFx1MDAwYt1cdTAwMTgphJRBkMI8XG5cdTAwMGVfK43LZySXM6/DuIUzXHUwMDA2SOE00iwqgFvwXHUwMDA2br9lnFx1MDAxM8f8IXekeJdTNeF1XHUwMDE4wLfgXYRwl5vDXGZcdTAwMWX2czNKp+JBnTxTY75cdTAwMDZRWp3770Yk4Vx1MDAxMzVcdTAwMTNcdTAwMTBDXHUwMDAwkeDFRChaNPVCXHUwMDE0XHUwMDAwkdpcdTAwMTNcdTAwMTTsXHUwMDA1kqJcdTAwMTli6SgjXHUwMDFht1x1MDAxOC1KJrSGWJZpXHUwMDEybFx1MDAxYndcdTAwMDBcdTAwMTCI3XCZrKVcbnspuVjO9SZcdTAwMWSnsVxc4lZcXFx1MDAxN/5QwThiyFx1MDAxZm5x9fJ8t5fe3TxcdTAwMWWc9rf26lx1MDAxObV3kruiOf10m1xmf1A878XCXHUwMDFmXHUwMDAxXHSeXHUwMDA3/EjS8ylcclx1MDAwMyeOUoJcdTAwMWLgWbDVQ3iM4X5qI7WFa+DBzcnpZknwlKz4KX7glVx1MDAxZKekcaGZgSBMXHUwMDEyKbDJYub4wy2cMfBcdTAwMDc1nlxyt4pcdTAwMDDyPW2RhoQybYSIN0uzbIKLaFx1MDAwM8El6sQy51x1MDAxZavQRfOUUYJTWyT1ilxyPltiplo078dcdTAwMWaL1lx1MDAwMuLrqFx1MDAxOcMjXHUwMDEzXHUwMDAwhFx1MDAwYo8kaf+ol2rt1+NSpX+cq1x1MDAxN9SpOWqIzH68Ko5cItJTyHHMkLdA+bA+3iTwkVx1MDAxZVhcdTAwMTQuIFCEiIG4lidcYuIxaqRcdTAwMTKSMMaUI3kpPVx1MDAwM3GE4cZcYlx1MDAwMuGRMEtNTVx1MDAwNljQVkqCLFx1MDAwZa74QYVnTJiF4Jj5kc7MXHUwMDExS+2xvkfYaye3lS/p/P399tbjYy1ZtypXXFxcctXz27bhhko2vjJcdTAwMDGhTlx1MDAxM8OEb8qdZVx1MDAwN4b70cXw57i4VUopkFmYYVx1MDAxN8mYXHUwMDE1XHUwMDAxXHUwMDFiI1x1MDAxNdxHXHUwMDBit1RcdTAwMTjm2IDkq5IsXHUwMDEzXG4xyFOpMXAvnVx1MDAxY85cdTAwMTFcdTAwMGKQjFx1MDAwMbBcbjhymjm6XHR9nUZKOrzWKVx1MDAxYzpOh94sOlPHXHUwMDA0Rzvux/3XXHUwMDE0oXfRK3wj01x1MDAwYpx6RFpcdTAwMWNNVkb5XHUwMDA3JN+9t/KE4Vx1MDAxY9dcdTAwMWSC71x1MDAxNo4pV8ZcctarNbdIXHUwMDFhXHUwMDAwn3BccnxRXHUwMDBmXHUwMDFlMFx1MDAxOGWLXFw7bDn1mpi9w1x1MDAxYVx1MDAwYjE4dVUoabA5e1jv0Nh4O91cdTAwMWHZKb33m9RXpll7qTe38merL6K/fV5M2LpcdFx1MDAwNmZcdTAwMDHyXHJcdTAwMTmKwj/Mxlx1MDAwYjsybFx1MDAwMmIvuODIxCVcdTAwMDZ1ejXxgOGqgq9xJUlcdTAwMTNccoRlXHUwMDBm3p5uWm9vx0KUr8rV9ctTftGlOXf2gOPYioCHwymE3dr4PvRRvMBaOC5PXHUwMDEzmlx1MDAwMVx1MDAwNKNTZjSit9KvjGRZKGVI3kWFsYxcdTAwMGIz7Nv8PCuKJ67h1FxyoZZcdTAwMWKpdVDJ0sVAblx1MDAwNYiBgbjmnrRoZpnROOI0aorBUFuqlcBSXHUwMDE1ROPB3dzGsVx1MDAwM3LZ9Vx1MDAxMYqANFhVYZUzh6vDXHUwMDExXHUwMDEwI1xm3OFcZnhV30sqX0ZAK0iQ8Vx1MDAxZfKvfH9CI7zlY1x1MDAwMmRxXHUwMDAxobFLi9DB/NXB6XP5sPd8tmuNMpc35dfz9XjpXGZcdTAwMDChXHUwMDFlZ9IqzkDJmL+jcaCGhnhcdTAwMTLCXHUwMDExtCrKUlx1MDAwN18gXHUwMDE2kyGWIWDlXHRTUjJHYEKWWplcZlx1MDAwMVFtObhgTV3xSjhcdTAwMDBC4lx1MDAwNWn0PGk/lMheXFydmrXCMcmyzd3nUuX08TJZ9lx1MDAwMiXr+/FPuFx1MDAxY499/evwJDxZkVx1MDAwMnChVEjNjeU4ri1cdTAwMWOMIKDv0lhcIkBQLLbaOIQwXYTgXHUwMDE2kVx1MDAxOFxiIcNcdTAwMTU+XHUwMDExgKRgl4wkY2SmcJ1cdTAwMTNsk3V0XSwhQsSGXHUwMDE5XHUwMDAz4Fx1MDAxMtPXzrgrXHUwMDE0I1x1MDAxMGmUooymT8ZhLE4pfVx1MDAxNSPcvE+I/vSpi4tcdTAwMTEmuHFnsmT00lwitPC12DivP2auNjNHpLK3ccbIXV6kkTMxwiNIzc5ccrKDXHRcdTAwMWRcdTAwMWN4Zdx6XHUwMDE4sFBcdTAwMDVgXHUwMDEyIKhxQFx1MDAwNK49nHCXykgscNJlj0ZCyFx1MDAwMFx1MDAxNtByQZxcdTAwMDWPYFwiZbg3gWA9Xug5Qoad9YvNNX22S0xvda+9pV67xcNmwpSJYvL7IUOGUu1cdTAwMDFw5lhtwuZcdTAwMDc+UvHgzFx1MDAwM22BoEtLYpg1fOLxwlx1MDAxNWVwvHFcdTAwMTVJXHUwMDEzk6SAPFx1MDAxOINcdTAwMTOnXHUwMDA2p1x1MDAxNlxyXFyDWIycXHQh2ljF4H7C7Vx1MDAxNIpbx1DvrFx1MDAxMZBb4mMgIK6ZJ5jmaHxcdCajRklUJZFcdTAwMWXAPUk5wH5qRTBd7UqSLMtE4Y1cdTAwMWZcdTAwMDL0VepcdTAwMTA299CysTWGa5zAT1x1MDAxYlx1MDAwML0nSaYq7X5cdTAwMDCghcmNhFx1MDAxN4kmQJNx3Fx1MDAxMysl0rh+qKvCy6lpXWR5o3JRaZt6L1x1MDAxNtzJcNycXHUwMDA3hopKXHUwMDFjRuB6NDFcdFx1MDAwMVx0qCTWulx1MDAwMS1cdTAwMWJug1x1MDAxZFx1MDAxZVx1MDAxNvBcdTAwMTJcdTAwMGVuM1BNhuy6QVx1MDAxNVxcJkSSJkTAb4BD1M7sZXhBSGKrIZ/nWpmX68NytrlvznJcdTAwMWI793tvz706P39IXHUwMDA2buA6fXTY31x1MDAwNW5CpXjs21+HXHUwMDFlM02H4PNcdTAwMDfvaS129Cnlo9Kdn/t3y0RcZvfPqCcp2Fx1MDAxOKFBJlxiNWNEXHUwMDFkSFY6wVx1MDAxNC1rJIlcdTAwMTIgOEQlLXPlP3h405hcdTAwMDVcXMmsYDPoXHUwMDEysZyrL1x1MDAxMXVcZpJcdTAwMDRcdTAwMDNqi19FmWSC23amQFx1MDAwMldcdTAwMTehipXdu9JZrrytz07Xqq/lx+u9TpXGzILgYiWBxUohhX9cdTAwMGLzOyrg2KFcclx1MDAxOFx1MDAxMGIgZmRQXHUwMDE1XHUwMDE5hG/SXG6IXFyFXHUwMDA2m0RcdTAwMWSayYVcdTAwMDdcdTAwMWbg8C5cdTAwMTFCXHUwMDExYZadI0nZPFx1MDAxOMTayIToglx04VxuTITlSlx0Nseuz92H547deVtrXHUwMDFhdlI4f7SnXVNM1vWpccfNVGue0k6CSFx1MDAwZmRcdTAwMTaEXHUwMDBme9+pldT//aBMTzxgqKZcZo5cdTAwMTeeVPk6XHUwMDEySaP8XHUwMDAygSCcNyf4XHUwMDFmTiNcdTAwMDbhxkj9XHUwMDA1idJcdTAwMDLSnG5cdTAwMTJcdTAwMDRAkOCaXHUwMDExJTVcdTAwMGWRXHUwMDBiaoMnxbGnnSDDKDdMSEpnXlx1MDAxNXIrQFxmUMT1XHUwMDAw9VxiXHUwMDFjdSGa+DnAXHUwMDA3llx1MDAxON7mUlxuJD7BXHUwMDE2viBFgV6u5E3UOMKZkpRcdTAwMTknKFxub1x1MDAxY1x1MDAwMXWH2JnZ9Fx1MDAxN8tgpoV9aVx1MDAxNmaAXHUwMDE4ulxie75cdTAwMTlcYoXnRCZcdTAwMDBcdTAwMTUnedngilwiVC6bp4Xr++rTdq9T3l3X9dP2WtaxXHUwMDA325lcdTAwMTMh1jNcXFx1MDAxOeyGXHUwMDA2taJcdTAwMDHSU+JcdTAwMTkpwapBnI471Fx1MDAxY1x1MDAxYniDSkclOFx1MDAwN8JcYqWcKoGx5lJcdFx1MDAxM+JcdTAwMWStXHUwMDE4IS7NXHUwMDE0obtcdTAwMTOkwlx1MDAxNmc7T37TXHIqc/n8pr4s5zNmrdW4XFzfv0mWXHUwMDE00cxQslx1MDAwMITs/k9cdTAwMDfFd1x1MDAxZVgkXHUwMDAxmykxODQoLMVcdTAwMDZcXKaYr//2kyaMXHUwMDAxatNcdTAwMDa3XHUwMDA2XHUwMDFiS0VwXHUwMDAyNvXtMFx1MDAwNIRcdTAwMGb8iaJcdTAwMDBcdTAwMDZcdTAwMTmETY5cdTAwMWXWWUNcdTAwMGa3OMaAXHUwMDFlXHUwMDE5QLFIrFx1MDAxNmpcdTAwMDYlQmGFnCdcdTAwMTC4c3/X5Cdz6tA+LDMyMcBcdTAwMDfOO1x1MDAwYiqd+2FCTZzFjbxgN9KnKtNS+aeCp8BcdTAwMWVcdTAwMWSQ0sXmTZ1cdTAwMDBcdTAwMTPGocfnXHUwMDA1RWjcfnZcdTAwMTfuydF69qhPr8/am0fNSu4oLvKQXHUwMDFl1Vx1MDAwNrmsjDBkbN6WXHUwMDFi6ykrtVx1MDAxMMaALfGPdkZcdTAwMDBcdTAwMGbGXHUwMDE1hFx1MDAxMNhcdTAwMWavLOBcdTAwMTVplug/IfBcdTAwMDBcdTAwMGJcYtG8XHTOwlx1MDAwZqwgXHUwMDBiU0z1/lx1MDAxOIcpgvRcdTAwMTRzsLlpsVx1MDAwMEJQzOZcdTAwMDFcdTAwMTBcdTAwMTIwdEF8bFx1MDAwMaxLLHRcIkxwsZtcIq0pU1x1MDAwNEwwXHUwMDA203ZcdTAwMDJCmOW0bYtcdTAwMWS8UKt7xNbPZety5+q0b+J1sE0wXCKKXHUwMDAxkFx1MDAwMzhcIrlcdTAwMDE4x3zF/882XG5uPInbgOFGaVwiXcnbZUk38Yy+XHUwMDFj7NFzJ2vDu005ZrBcdTAwMDBFznHIr5a57W+S4uFBqXh1unq0azk1Nln8Ylx1MDAxOJ9u3DdV81x1MDAxNCrHY9/+unWaYVF3MFxiJ0FcdTAwMDaEkFx1MDAxNuFcdTAwMTh30DLPPIhwy0SMIEJZb7BccktcdTAwMWFlNJjVMVtE6Fx1MDAwNFtkXHUwMDFkgGZcdTAwMTlBhLd0QVxcXHUwMDBiLsw6R2nCQ1xiJTjuTLUz2OhkLWFfr+m+w+5fQUl3gtt2lnTHLy5CXHUwMDBm72509eXuob1Wv8g3SvfP9YOX6mMsVFx1MDAxMN3XXHUwMDBlgSdusFx1MDAxM0ZQjizwQUWMQ1x1MDAwNcC0ZyVjguHuNmV8d+m3rKdJqI8pU1x1MDAxNm6xXHUwMDEzJIS2tStKsd+HTlx1MDAxM2dMiVx1MDAxMY7z5Yd183xpm49cdTAwMTWiT8xt7/g62f5rg7uTvlx1MDAxZiNcdTAwMDRcbrp6ZIdTQKInXHUwMDFlL5pcdTAwMDcg4nhfRyGpNJBpXFxfSsApM2RcdTAwMWOlMsgkOPe+dkBAmlx1MDAxMkuJQk4la41wIKC5U1x1MDAwMbhVIFx1MDAwNiTiVntcZvdZwFx1MDAwNXHjW3g0MMSce4JcZmZcdTAwMDA1botcdTAwMGXOXHUwMDE3cVx1MDAwN7vhXHUwMDEyXHUwMDExRTS5Y2O5XGaZXHUwMDE1XG4lV6ZUaaVcdTAwMTTjM2hzQ4aBqao7n5CoX/nRXFyEim44XHUwMDBlmlx1MDAwMFRcdTAwMDI46POKXCI07pJcXO2UWye02Xnb2e9dZVx1MDAxZa5ub2Nu3Vx1MDAxZcxcdTAwMWFJxdBCcCrJXHUwMDE4XHUwMDA1mcW8KmXwtjTMOlpLmYfzXHTI2s7BOirJXHUwMDFkQ31Uck9zXHJ2SHKrmFqin8RDfUqAYXSmWcM3WFJCOTij6Xovpi3xPvXtlb5uXdR3y2Qrk33au6juJIM/1Fxu3338LvhcdTAwMTMu1/hcbkj0PNBKkj1cdTAwMTCEXHUwMDAz8OVcdTAwMDSwXHUwMDE0wVZVxoNoRXpEXGJBkC5cdTAwMTi7pNSUW7iT5JlcdTAwMDVcdTAwMDTgIMtcdTAwMDDIOd5WV55cdTAwMTmwipGYXHUwMDExUYwyOvN9VW55jYFVMkwxT1uKgSOgXHUwMDE1uM/jNWCkkbBIXHUwMDFhOGBU8fm1z1x1MDAxYfDQ1C7xymS8ojRcdTAwMDdJNU7uxlxiuKJcdTAwMDQygFORfq3J4F61L1xy5Vx1MDAxNVx1MDAxYq1uebGrwFx1MDAxM6DFOFxcXHUwMDE5XlGE0lXbXHUwMDFiJ3Tvsd7Tu1x1MDAxNXad1WIjv8riwlx1MDAxNeaBXHUwMDAxYVx1MDAxNNfaMWlGJ2EgtPQgPCNcdTAwMTBcYkH8IFx1MDAxZISpztWZXHUwMDEyXHUwMDE0WVxmSI9cYvjMJeVA0lxujuFS+Dm8hnpcdTAwMTncdfuhl1x1MDAwMjdZXHUwMDExoeY4k9etmfpFsbPP9VU/9/S0xo7X2ufJsFx0x0Tht2OTsc2ZY+I7XHUwMDBmKJKo00viuFx1MDAxZKgqwe5hpVxcSyGENbjXUVx1MDAwMfSTbNpO+Fx1MDAwNEsh4DFcdTAwMTJGXHTHpK7FWa9vqFx1MDAxY7nFMVx1MDAxNvSQKsJcYmpMlDCGXHUwMDFjXGY4XHUwMDEyXHUwMDE4tIEu3LFsfFx1MDAwZs+TYLeXXCLuvd0sXHUwMDE0eFxmglx1MDAwM0Zn0HxmuPV7qWlxx3djjvCm91x0+MCJOaI0TecrveY+adzfXHUwMDFmZrf42WXvKPdcdTAwMWFcdTAwMWJvUM9cdTAwMTCMVlxmVoHYeMuIUZ4yhjNmNVx1MDAxMsBcdTAwMDTzI1pcdIAk2ipsizfKOIZOli0jSVx1MDAwMYfAJXxGXHUwMDA0XHUwMDE5XHUwMDEwXHUwMDExXHUwMDAzhreMXGJJkWV/nvN9O4U7XCJcdTAwMGXrNXlzVro9PVxcXHUwMDE3O7ncblwizGEpzrl8O+ZcYpXjsW9/XHUwMDFkc8y2ZVx1MDAwNJwmUippKsFzMq6CXHUwMDFiMmNcZualzVx1MDAwYuRcdTAwMTSSXHUwMDE4SFx1MDAwMNfg0UH/XHUwMDBirpDwUS29XHUwMDFiJ8k8XHUwMDFhaZwodZRMllx1MDAwYqHCoYBFwVx1MDAxMcI5XHUwMDA0XHUwMDE3XFzL+1x1MDAxOetALEr1XGZWZnPKqY9tbPr1XHUwMDEx71H74M+lcvGmU64sUE9J+HqoXHSO3b1WXCL6WiPU9F5ns5m9rdpR7ziTaedk7TKzXolcdTAwMDcjcDWsNFRxzVx1MDAwNdeGj5Y2x4osKpgqdE7NMU9qpiRcdTAwMGV/XHUwMDE5f9vKb1lrk6yXkFx1MDAwMilLXajehneUoCVVZp5cdTAwMTDiiYOBzzzJx61+R5x0bs7LN7KdXGZCyFx1MDAxMay0XHUwMDEwaYuA+KZcdCHSqKBQLYxkhFx1MDAxMVx1MDAwYmG2kT68466fkCmbPVx1MDAxMtRPcFx1MDAwZsKA2duoXHUwMDAxhlx0XHUwMDBl8nFP4m5cYq6EXHUwMDExXFwpaWa+y9ItnTGwS4Za7pFQm0hcdTAwMDFvXG6Ni8qRuIlcdTAwMDS7PXzcWMssxmToXCJcdFx1MDAxNVx1MDAxNu+lw9yx0JBcdPkymdTpMzpbbJOait3sXHUwMDAzuPSbgy99MzZcdE9iTEBcdTAwMGLj2OTn5URoWunViNrFXbXPXHUwMDFljmXHXFxs31/xmMNzVOGWXHUwMDFkXFxqXGbOSzIqR2uVgem5OPhDXHUwMDEz49HB+lx1MDAxOU5AhdmyqSMp/iCWXHRKqZNmLGrwRVtcYk/9wGXmXHUwMDEw5LLRzJYq+fJTX+Wu83l2eL7W6CeDIINcdTAwMGWDxYIgXHUwMDAxXHSeXHUwMDA3XHUwMDA0SVI5oUaA9cWhQa41k3TSXFxcdTAwMWXiqelASILSXHQ1gD9wry2ANqJ8bP8rc6ucuMUxXHUwMDE25jBcdTAwMDZrI2GGUCO5m0TgXHUwMDA3XHUwMDAyQY1cdTAwMGVcdTAwMWHCJepIljChhlx1MDAxOSyxufnoQ2FcdTAwMDdcdTAwMTX4dKxJv2nDamX1l8ZuXHUwMDE2XHUwMDFkeEzAXHTJgVx1MDAwN2GvtZe3685urX5S294n3Y1zyuNcdTAwMDJcdTAwMGaCPf9cdTAwMTCSXGJcdCje1/L1rm9aelxmVE1cdTAwMWFCccVYcLhGWerhXHUwMDBldlwiXHUwMDA3/anaoX7L8knS8lx0Ulx1MDAwYqPTcCmlXGZn08A2YSPVNMHAtCzKm2frxfXz3dPT1Y3XRnutXHUwMDBiXHUwMDAx5lUy3GGI5N/Pjlx1MDAxOCrGY9/+Ou6YbfWEw/lTK7RcdTAwMWFsQ9ZB3z86XHUwMDAyw1x1MDAxZEmRtEmVnVwiXHUwMDEyXHUwMDAzXHUwMDBi4HhcdTAwMTOVSH3AcG9cdTAwMDdcdTAwMWLLP2hcdTAwMDGWK9I0UeGIipbFk3AsoCniLudKhfBcdTAwMDFcXMpcdTAwMDGkMXhcdTAwMDKp5yA4MdRfMpu2eFJcdTAwMWGczOfypVx1MDAxZs13j/r7WmWlU678+c/kXHUwMDBm341cdTAwMTXCXHUwMDBiKFx1MDAxM1xcu7OAXHUwMDEy43oj9FVl1jeeb4rZU/3SzFfKdvdG3l7HXHUwMDAyXHUwMDEzUnqAyYk1WoDrYmK068kyg/FcdTAwMTJcdTAwMTXUYkQkXHUwMDFkbU9cdTAwMWEr2FxcXGLMOVx1MDAxYeGofC6RRFIkYbWVmmrhUmpcdTAwMWJaXHUwMDBmNVx1MDAwNlx1MDAxN0jPtYhynLl+rJdcdTAwMGV3My9PzbXt2t1ZdvPqLFx0kuBI0cC+v1x1MDAwZiNMise+vOhAXHUwMDAyolx1MDAwMUU4YDM64EdcdTAwMGXuiOJcdTAwMWUoszZKMFxcN26kg3Eo7bFVp4jEQFx1MDAxMoxzT1x1MDAwM9JkzDCNSyZGXHJcdTAwMTNyXHUwMDBlRVx1MDAxYSa/91tcdTAwMGWCxFx1MDAwMFx1MDAxMlx1MDAxY4k2XHJcdTAwMGLmXHUwMDBm3mFdKJTAjfZWKZn+fiYwXHUwMDBl/v6aaaFEoVTy9yPcfH/vRfhgyFx1MDAwNEfuhFx1MDAwZY7ri1BIvbq2/tTr3/Y7stDJNJr5m6tKzLZNnM3SXGb7b9HEXHUwMDE4PqqSSFNMmJZMSVx1MDAwMnaUxJtcdTAwMTOhUnnIm06ohd+0f1x1MDAwMPq3rKFJ4Fx1MDAwMqgtV1JcdTAwMDZ3qOC3QvVWXGZGXHUwMDAz0l8rXHUwMDBiVoRcdL5wfVx1MDAxMeNSlqZTT6MxgitkjdaWXHUwMDEwXHUwMDFj/NF+XGLy2Vx1MDAxOYFLYCFuxqWigtlcdJ1cdTAwMTGzJFx1MDAwYuxtVDOGnlx1MDAxNOXmzrOSXHUwMDA3+S1ZofmYNlx1MDAwNICWxW1cdCC1hFk7XjM1XHUwMDEwjlxiXHUwMDAx6EQxxWUspnNNtGe0XHUwMDAyPZBcZlwimWXdIKlcdTAwMDFhXHUwMDA0l7JT4epiUKHlXHUwMDA0iltHJFx1MDAwMMk5XHUwMDA2XHUwMDFjpFjtrN9s1Fx1MDAwYp2349ft/b3W/WMyXHUwMDFlII6tu1x1MDAwYjBcYj9WMlx1MDAxZFx1MDAxNeB52KYkXHUwMDE1U468JMooXHUwMDAw4Vx1MDAxY7FewDRRrF9cdTAwMTIqXHUwMDA1pn+MoWxKqvNcdTAwMDRcdTAwMDVTrsBGcmpcdTAwMTmB2M2M0Ch9nlx1MDAxNETQklJuMHyAP1x1MDAwNau4KZtFt3TGiHUyXHUwMDAwqCB4XHUwMDBiNYuDmrpkwjKwishKXHUwMDE0MIvGUcJZZk3Dmc+RwpypYEcq+qDQ4TNcdTAwMDZcIk5cdTAwMTlcYttcZkJcdTAwMWRr9HRm6SPUKfXbP5rN8vNKpfSP//RcdTAwMTFcdTAwMGJ8f6hcdTAwMTOeJZ1cdTAwMDBcIsZDnbDri9DHi5P60cZxpU5KtT1a3GqevFxuptNcYnWM1Fx1MDAxZeio4Fx1MDAxMOxwK2hQXHUwMDFmpVx1MDAxNZ6kXHUwMDE4XHUwMDA3XHRtLFlyXHUwMDFhp8HXY7jA/kaX0kZsZCHYRqynanqYXHUwMDEypahqtXrZvs1nzoqHl4c7by/iYHUzXHUwMDE5StGG8+9HKaFSPPbtr8OU2eZFhVx1MDAwMeSipLFcXFx1MDAxMFx1MDAwNWgmXHUwMDE4XFxRz1xiyVx0t1x1MDAwM+ZcdTAwMTm44JmvaHNcdTAwMGJJXGawQLWEXHUwMDBiYtpcdTAwMTg1mLRcdTAwMWat2Fx1MDAxONxcdFx1MDAxOGmalonRZFiBYmtcdTAwMDEgYPfi2lCwILDHXHUwMDFlZ1x1MDAxZNPHXG5G+iYnflx1MDAxM2nRXHSePFla9Hc/LdUvhXb7tFx1MDAwN/f208r88lQrP69cdTAwMDWl+e8qg1x1MDAxN35/oMwoyeWB9/rL7/7y/1x1MDAwMXjDmGwifQ== + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19SXNi25be/P5cbsWtySvX49Reu98vqipCXHUwMDEy6iXUXHUwMDBiSc5cblx1MDAwNaJcdTAwMTNcYlx1MDAwMaJRV/HGXHUwMDFleeSwJ3aE/4EnjvDkRf2bctXf8FooUzqcXHUwMDA2OCSQR5S5NzKVXHUwMDAyXHUwMDBlm332t9a3+n/6bWXl995ru/z7n1Z+L79cdTAwMTRcdTAwMGKNWqlTeP79j/T7p3KnW2s18Sk++He31e9cdTAwMTRcdTAwMDevvOv12t0//e3f0ms99tSqXHUwMDE1y16x9fD+tnKj/FBu9rr4wv+I/15Z+afBn75cdTAwMGbqlIu9QrPaKFx1MDAwZt4weOrzs4BcdFx1MDAxZPx1rtVcdTAwMWN8MGhcdTAwMDZWKW7sxytq3Sx+YK9cXMKnK4VGt/z5XGb96ve3p5vW29uxlOWrcnX98lRcXHQh9/m5lVqjcdp7bbx/rULxrt/xrarb67Tuy/laqXdHn1x1MDAxZfj9x/u6Ldy2z3d1Wv3qXbPc7Vx1MDAwZb2n1S5cdTAwMTRrvdfBN2RcdTAwMWa/fd+GP618/uZcdTAwMDX/pYTwlFx1MDAxNZ+Pj2fp/Vx1MDAxOaNcdTAwMDMrWW81Wlx1MDAxZFrJX7HB43Mtt4XifVx1MDAxNVx1MDAxN9Qsfbym1yk0u+1CXHUwMDA379Hn656/f0ettac+fntXrlXveoMlW+VpK60xwlxip4X8XFxBebD34KRcdTAwMDFrhHVcdTAwMWbP0Oe2d0qDg/CP/t1plr7vzo+T8Xk2xPff/PnzXHUwMDFi0Os3gmfKf658d/uZ71XrncL9Y17kN9ft68F6rlb7+JZDh7DQ6bSef/945s9/XHUwMDFjdd381cHpc/mw93y266y2lzfl1/P1XHUwMDE5XFz3tdg4rz9mrjYzR6yyt3HG2V1ezuC6jeuHui68nNrWRVY0Klx1MDAxN5W2rfdmcN3K7l3pLFfeNmena9XX8uP1XqdcbjO4botcdTAwMWa8gDM95urnqnW5c3Xat7PYh7tcdTAwMWJTfbl7aK/VL/KN0v1z/eCl+jiD65p8pdfcZ437+8Pslji77Fx1MDAxZOVe+Vxmrqsz61x1MDAxYs83xeypeWnmK2W3e6Nur2dwXcZfay9v153dWv2ktr3Pulx1MDAxYucgJrvu958+XHUwMDAx3G+XXG7v8lx1MDAxNoyRTFx1MDAxOXBcXPpg36g17/HJZr/R+Pxdq3j/KaJ/8y04mXLQlsfpXHUwMDA2q5x1UqpPSTpONdjMo9uovj1cXOv22eNen5lqt3WfdtVgtPCEiFVccsIzKrCW2SlcdTAwMDdwXHUwMDExqkF/ruC7KlBGSYHq4PPFXHUwMDBi1Vx1MDAwND9cdTAwMGVSr/xcdTAwMTJcdTAwMTB577e91F8tvOXenlx1MDAwZe9vt0/a+0ruskZ1YsF6X66cXHUwMDE5c5upm3Lj8UzW1y+PT9NcdKSh7+/DXHUwMDEwfk4sv5JcZpjEteiJQVx1MDAxNL2bKVx1MDAwN5Fl2rOxIFx1MDAwMu3xOYKI2TCIeFxiRCCV4aBcdTAwMTUkXHUwMDAw0dBJXHShZZZcdTAwMDfv8/62mr3T2lx1MDAxYm0xZ0O/3Sw81Fx1MDAxYa8/jIf3k0knXHUwMDEy9+fkaH3ltNzBw+ffxm5cdTAwMTk/d3Bcbu3QO1ZcdTAwMWK1Kp3b34v4Tfxvwff3amiyfLzgoVYq+fVGXHUwMDExl1HAa3Z2JpH3rU6tWmtcdTAwMTZcdTAwMWFn4VVOhTMw8XaMZJI7zmFyZZU72Mvl9pt5efb89lbL7GW7/duTtONMSTVKWSk3N5SJXHRRZjTTjFx0s4wwy5ZcdTAwMGJcdTAwMGat5lx1MDAxZo46tYdC53Xlodwr4IJcbiutZ4TEXyeCXqNcXOmNXHUwMDAwXq/VjkPd0LdcdTAwMGJCbMxcbqfniSB8XHUwMDEyPFxiPq6NQ2PDTVx1MDAwZb69+8dG/+D8rvOWq2w1rp/ebq7Nc9rBh2fOY1o7Q8Y7XHUwMDEzTFx1MDAwZYHPMuc5zlx1MDAxNLNcdTAwMDKsZIzPXHKKYFxcXHUwMDE4iuDXsO9YtJIzgfclnazx+u5J5G9q5uBmde1WXHUwMDFk7J+vvfLmpKyRN1vl7e3N6tbuZbmVhZOdp2y/+7VYI+C9iUOUNKjKXHUwMDE0S1x1MDAwMKjo3Uw5oCxTnopcdTAwMDdcdTAwMTTIRVx1MDAwMVxudFx1MDAwNKAgXHUwMDA0KDBKSyntUnLI20br9uau0ETC96dvuFel+1x1MDAxYvrVf/jWXHUwMDFjPFMrfWt2ypWbXCJ+g963pud50frOx8BmyjXHaIygXCJM/m1+QjeigVx1MDAxN4dkhyjXTsHk/vVOfVdcdTAwMWNuvD7ub+3kN3NH91x1MDAwNzs5eZh2JJNqjEey1taTi0GyjUCyNkEgK2GQoaZWNW6eXHUwMDFl5+vHtlfK7LD64fHaW/lSXHUwMDFmTuxZz72uNVf72/z+pnnN7s3GW//5dVx1MDAwNlx1MDAxZc86PLm7VruVe7rTx1x1MDAxN2fF7ZfOU3FcdTAwMDbXXVx1MDAwNlXul3pBL6pgXFwrISZX5dF3P+VcdTAwMDLAcjeCXHUwMDFia8dcdTAwMTYkXHUwMDAwjFx04z9spXJcdTAwMTBGSWelSCBcdTAwMDC+ilwif9d6/tu7XHUwMDAwV9BcdTAwMTitXHUwMDE1VM9cdTAwMWZrnF7rKt9tXHJcdTAwMWGkgoxRJZWcXHUwMDE4dKdqZy+3/eTeNo/E0XFmg99sn52lXHUwMDFkdFx1MDAxNLpgXHUwMDBlzW88ycBccoMh0KHx51x0py1X739aOzfUgZNh2Okwf+ZcdTAwMTbIWSB/UUh7nN59LOzDqVx1MDAxMsfbpbKp5V+ea1VXfZhUj+WON7rbl2dHd43Htcp2ka9e7a9tzUA/qnqnXFzYut9cXHONs57cXj+7Pcq0Z3Dd/Vxc97Rw8Fwicmf3V07vrV2VO83O19K7eOjjRIDmXHUwMDBlj3yCtJboe592XHUwMDAxYISn4lx1MDAwNVx1MDAwMJdcdTAwMGJcdTAwMTNcdTAwMDBGhFx1MDAwNUCE3uXIujUkSmn5Kmp3YGP27lx1MDAxMKylm8G3Wqj+XHUwMDFko7+C+je82OlcdTAwMTUx2HjPsJJcXGtmXHUwMDEymL97XHUwMDA1Y0pcdTAwMGZcdTAwMWS5UW83+zu9yuFm58ykXHUwMDFlh1x1MDAxYZEm43BcYkx59lx1MDAxNypipYM4XHUwMDE0XHUwMDAwhlx1MDAxYpGE/S5QXHLfV0/V7na5elxye5ndvFrrXnTW+Vx1MDAwMvJcdOal3tOiLkHFXHUwMDAz1Vi0yiQkUJjRdynlQLVCejZcdTAwMWWonC9KYaqJ9KU0nHG+jN7mI1SJi01WXHUwMDE4o1iCXHUwMDFh8n2BI3H2jvNkXHUwMDFhXHUwMDExKalVXHUwMDFhXHUwMDEy5CmMXHUwMDE2aGnFmXSelGiDW81BXHUwMDBiXHUwMDE5cFx1MDAwN7FhfTjPXGY7XHUwMDBm6TGTXHUwMDA2KVxiXHUwMDE3uPm+mNtcdTAwMDfu5HCO3490IVx1MDAwMUy6n1ORfLyKnFx1MDAwYiS7vUKnt1ZrlmrN6vDCvtdcdTAwMTJMkr8zXHUwMDAwcbFPq2RcdTAwMWVwZ6xcdTAwMDHFjNSCXHTle1G10Fx1MDAxZWy0lkO3NPTVy83S+CWNRqlvSVx1MDAxOeYxhJJcdTAwMDNrrNRcdTAwMGW3jJmIRfHwOlx1MDAxYYVub7318FDr4W5cdTAwMWa1as1ecFdcdTAwMDfbt0rovkOCXHUwMDFjfFx1MDAxNr+H/7mgXHUwMDE4aNNcdTAwMTWH2c/nTyufOFx1MDAxOfzj4+d//GPkq+OPLz1cdTAwMDZcdTAwMDf381x1MDAxMr/5/04stLg0wd9+sFx1MDAwM6aEkuDPXHUwMDE1XHUwMDFlm101ki6lVWopJOpaOaaF0CroxFx1MDAwNkpcdTAwMTTmi6HxnjXCMXDKKYZLUZ9cdTAwMWL/IbWE8lx1MDAxNLjPh28171x1MDAxMlxmz4wwXFyxn3O2pVxchE0sL1BcXFx1MDAxOFx1MDAxMFqjaFx1MDAxMlJcYlx1MDAxNFx1MDAxY8L3qlx1MDAxZuJCXG7me2g7nVx1MDAxMFx1MDAxYm2MXHUwMDA3hFx1MDAxOJOMMzybinFcdTAwMDUoWkOrkp5SvkVcdTAwMDF8cYlcdTAwMTZ3tOlcdTAwMTE61Fx0pduYWofY/FFcXFx1MDAwYtdCiclcdTAwMDNcdTAwMDbmcL9+e/D8em329tqHN1x1MDAwZs+H5+s67Vx1MDAwMk5b1OTWaqc5kjMhP/eD3i9cdTAwMTVcdTAwMWFHXHUwMDAyb49xiF1UqlwisLBcdTAwMTlcbjhcdTAwMWWVwiZ9ccHvLFxm+YbkJrWlXHUwMDBmndLt03V360StnZxddU5Nfb+UN7/as/9cdTAwMTNcdTAwMTlcdTAwMDA/QDZDiT5cdTAwMTKvsSVcdTAwMTUuXHUwMDE2qpxpY5FcdTAwMTQlICPRdyntWHVyXHUwMDA0Vlx1MDAxNUhP+LE6P6iyXGKTKVwi8dspi+tQPFx0Ur+Kt+KhtPLuLO/693HuLosxOibosvCtcnolaUxsTE2AYVxuIFx1MDAwMfBWnzaKzUJpXHUwMDFmSmf6/t5cdTAwMWVmq1x1MDAxYu2rtFx1MDAwM1x1MDAwZsGGXHUwMDFjXHUwMDA1bX9cdTAwMDD8kVx1MDAwZpdYSCXSpiM5d9ZxrVNcdTAwMWFU50/XZyy3Wui6p6f+eie3f1x1MDAwMye9X510tlx1MDAwNMFvbePtdcnQvEE9OTmdjb5LaUcqt/FIRWNqQVx1MDAxYTKqXHUwMDFjI0JBolwiZaCZXcacs1rru+pZqH5cdTAwMWOjXoL68XORI1x1MDAwMVx1MDAxN+8h07FcdTAwMDVcdTAwMWJGOk05Z5OrxtHkP6WAQzR5KFaASeRcdTAwMDJcXIFcdTAwMTlCXHUwMDFjN8ZcdTAwMDOfXHUwMDEzeI6IM1x1MDAxZW24ZlxcUqmhjirfMNrTXHUwMDBlXHUwMDFjcCWF4cBCQXCwWlkw8idcdTAwMTVnylx1MDAxZGRJfFHc4L11TmmLXHUwMDBm5lwiXHUwMDFj6tab1rE/msuu+Fx1MDAxZHWcIcdcdTAwMDTOOf7JhP70bH4s42u7wDKxx5dcdTAwMWXhg/t5vd/8fyeXXz5cdTAwMWZxiDFQlEE6NzljXHUwMDE4TaFSK8Ckx6Ri1jjlXGZcdTAwMTL5gFx1MDAwMFNcdTAwMWXikjxcZkZZodn84pJWeUrircdPsbhcZlx1MDAxMSnBlIdrQcLAXHUwMDE5p5z6XHUwMDEw86eVcuPsz5WxLItcdTAwMDBjXHUwMDFlXHUwMDA1yoRA7DiG2DJahcOU2jOWXHRcdTAwMGLIXG6Jjk3p4Fx1MDAxZk08hoWZkVx1MDAwNlx1MDAxN1x1MDAwM1o5gTI1LMu45yRIaZzmnFx1MDAxOatDK/pKoi32WNMjfKBcdTAwMTNKttjEJlx1MDAxYltcbmCkwNstXHUwMDEy+CzWXHUwMDFiNa1Nj/dLorB9ud576Wy9XHUwMDFlp12uaVCeXHUwMDE0sY59TVxyWLgwxjpNoaa5iTU1WSEt3n/Uf8hcdTAwMTOXMbWpiMS+V/7WLFxyjtm3Zqtdbn5rXHUwMDE2XHUwMDFirW6Zik67dL1IK2nq0tlpu0VMstLpXHUwMDAwKWNcdTAwMGJiOUPhKzhP4Js4a3Q07+Lze/zx+e35dXWr1k99QVx1MDAxY7V9XHUwMDFhguQw1ZAoXHUwMDBiXHUwMDE35EeUXHUwMDExJXEuxCVcdTAwMDRcdTAwMTfWmkRU4qtcdTAwMDCSTP5vzedOrTc41pW/+ZvBX5lMJFxmfVuwXHUwMDE4XHUwMDE0xi9vOpovff6oIM1cdTAwMDeFXHUwMDA2XHUwMDA1WlZcdTAwMTNjb3QsMaXYM8Z5mkkw9Fx1MDAxZlLCYW2oXHUwMDE5UOrYXHUwMDAyXHUwMDFjg8iGXGZIZ6nxXHUwMDE4YotFXHUwMDAwUaPFgYDQyItcdTAwMDRyorB3X2gprYZEmvLLcfxcdTAwMDTOXHUwMDAx4axcdTAwMTO4KY5bx5zzXHUwMDExzI90mYicv4lY/ehcdTAwMWHW4VVwRVx1MDAxOeRagUJTw3JcdTAwMTlaXHUwMDA298i1xMTgTzTS5Ffn9dEnmVx1MDAxZaEznJDWx0syPaLhXHUwMDFiXHUwMDFlTY1LmjyRenQsKaWizCnnSVx1MDAwNVx1MDAxYThcdTAwMWUkXHUwMDBlMFx1MDAxY+NYqCgjZ6lcdTAwMDVnmcKfotpsjFx1MDAxM2WofKRQ3Kml9ldM7Fx1MDAxYsgwT1x1MDAxOdwpgzRcdTAwMTCVlPJZsVx1MDAxZlJEzV+Ykd/X4UFCXHUwMDEzUqFElUZGuE2WS5plYk8zPeYnzuLLQjSAXHUwMDE2wiQoXHUwMDBiXHUwMDE53TYlpdLMautcdGp6r/G0+UMy9HaUYp5EecdcdTAwMTRIXHUwMDE0aD5cdTAwMDNx1sKMyk+Mslx1MDAxNilcdTAwMDTSXHSwOlwiw8l8rvy7+NJKUGxEL7X0SlwiNlx1MDAxOFxiLTQuXGak5o47iHJuMqN+lNSRu3o6YTa6+ddcdTAwMTAzQ0tcdTAwMWKclIZpXFxcdTAwMTaQdI1YlORLJMziTzM9jO9cdTAwMDJcdTAwMTOJr5HZYVxcxFe2aaU5N1x0Ol03XHUwMDBixd7rzeXrS11Uq92b7dfi+cZe2kVcdTAwMTjZlswg7zRcbo+8NoFO18xpj8tPITbXVmf+rtoj2q5wxq00qe12tp993bwwz1x1MDAwZlx1MDAxNb3qTlx1MDAxZds2V292clx1MDAxM3dcdTAwMGY7h1xcvr79en+2nt1mXHUwMDA3lzl+0Kl/rUQuiFx1MDAwZstqrSTTXHTaXHUwMDE4Re9lyiFlpfC0c3GQXHUwMDAyMFx1MDAwYoKUmqh5mEWdx4R1y1x1MDAxOLvIXTzEXHUwMDA0J+aUwjVGXHUwMDA3XHUwMDA0faWD9U3HvsUoZ1x1MDAwMiWvK5kkX3Kk3Ekr0NC0XHUwMDExPE53OSE848PZ/DRcdTAwMTeiXWoyNCVcdTAwMTJI4Fx1MDAxMEG/0Vx1MDAxY/X8VW9RvVx1MDAxMVx1MDAwMO1m95O9xFJCx4PscTwlXHUwMDFlXHKcXHUwMDAwUVegtWTIwZGkXHUwMDE5bX1cdTAwMDRxKVx1MDAxMqriXHUwMDBm1ODZ0FGaJSNcdTAwMDan4/OwlaNUOpNghJLaOrl6y2aq2atrtXet8yZv9lNcdTAwMWbrzKDV4SkhjGBA9bVDYiVjrVx1MDAwN1L+6LrC5lgwgex7+E5HyFx1MDAxNebwNChwZI9cdTAwMDJaK2H1Pqh1l+xcdTAwMTdVXHUwMDFj/vSYtbTQWj6iPkFcdTAwMGJrrHNi8m5DeMJOV8tPu09cdTAwMDess406LNe620p9e76McdZzzH7HXHUwMDA1d8P50lx1MDAxOW09XHUwMDA27lx1MDAwM1x1MDAxOTa4stlBg4uJprdovCVSgF7GXHUwMDFhvsPeXbmzctRpXHUwMDE1cd1/OC0jXHUwMDExLVx1MDAxNTqvyca2zC1cdCB2ddNrJn/KSahBLofBRLnJXHUwMDE10zZsXp/s7lx1MDAxZbX3rnbN6nk2qy9fv4JiQmn/6YHUXHUwMDAxj/NQz1x1MDAxYSfm6KzxZX5+XHUwMDAy0IZcdTAwMWHTg0A7WFmeyLhcXKCvJrt3+XLx8FCsXHUwMDFmPl9trVx1MDAxZZdyXHUwMDE1c92Y1Fezddpff3xQa3e92rMplfZcdTAwMGXrO+tqXHUwMDA2xXy5vevdTqO3vrN6vy1cdTAwMGLPL3x1ffd+XHUwMDA21y2e7FRcdTAwMWHnmcqlyZRcdTAwMGJnpZdO5rhyOIPrvu7B6mll9fFFXHUwMDE2Xir3/GYre17uz+C62TxcdTAwMTSu76tP271OeXfd1E/ba9mrXHUwMDE5XFz3kl3tlFsn0Oy87ez3rjJcdTAwMGZXt7dcdTAwMDczuO69yWYze1u1o95xJtPOqdplZr0yg+uuul7xbLNfaq69XHUwMDE2Wju3m9nii5zFes3q2vpTr3/b76hCJ9No5m+uKlx1MDAxM86oTVx1MDAwYilcdTAwMTNcIjY3TDpDXHUwMDEzX/nk/vtoYZB2lSDB+YNSaFx1MDAxOFx1MDAwZulcdTAwMDThXHUwMDAySmFuOkHARD0gyclhpL+6dXk42Wq73UD61MNjuPKHlaNWt1ftlLuPjZW/XWlcclxiUXZtxvxsjDtyXGbLXHSytonWPz2Dszp2rFxiWOk0hVQnh+vVYSNf7u/sskr7XHUwMDE5qeWTfOvepX6wLDI45aG1LiUwspBcdTAwMDK5nFxcKc9cdTAwMTgtXHUwMDA3jVKUnKPPXHUwMDEyXHUwMDE491x1MDAwNmYrw1x1MDAwZiNcdTAwMDM2jF1cdTAwMTBcdTAwMTDVSFJcdTAwMDFIzqX+RZRuvlx1MDAxY+mhu9Y4ZkLDMd843O2e9qovdlx1MDAxNrr2XCJr7Ev5xFx1MDAxObP2XCIyl43M1Wv9PMXc4LF12LtVrbcjyHbXxdrp5lx1MDAxYnPVXHUwMDE5XFxXZXeuNurrO+dXj3fnu49cdTAwMDenl7nLXHRcdTAwMWRBv4TbzouD7md3UetcdTAwMWOtZ4/6cH3W3jxqVnJHKea21fbGXHTsPdZ7ZrfCr7NGbuRXJ+SKv4Qzl16trF3cVfv84Vh17MX2/ZWYxf72Nqoowk+KanPnWauD/JaqQP7/c/ExVYtgXHUwMDFj01K6yf2j/XrOds/Kl1cnO2f79aP1y72DvUzqlTsyW49cdTAwMWJkuFJcZrLIPvf8vVx1MDAxY5t5VFx1MDAxZa+001x1MDAxMo2T+blnfDxrZIszhtrfLmdD9u3Dw73ZUu1pXaGDlYxEVHzjdS5jMaVcdTAwMWRlyU1cZqjRzqrUXHUwMDAyXG4pqGBWc4YmrvSfn/dcdTAwMGJwj2Y8cOBoPqCpPz++LD3GXHUwMDE0tfAw3FlEcESimnPac1Jx5lx1MDAxYzeGsVx1MDAxMN6oXHUwMDAxLLVqmHvCbbSiXHUwMDE551x1MDAwYs28iFLtObO299w73m7v156fttVuYj3jK2CZfTbvaHN2xZ83a5WwzjhFWaTK1/Nx5UeKgEFDi5Fz2qJJpo1cYlx1MDAxYiRDeVx1MDAwYsPf4lx1MDAwYiVcdTAwMTPEnlxceoTO7OfVfvP/nZxcdTAwMGLE12xcdTAwMGVyNvxtcMbKruiTmXrZpZ1nmWYovvAoKt+wmkE7N+s84ZjkXHUwMDFh1aJcdTAwMTV8fs3XbUQ6UsQoM4Y3XHUwMDA2hrpcXM2ZXGZMJymmXCJcdTAwMDP/9t//1//95//6r//lP//LP/+PhfrfxmjdIFVcdTAwMTha50jgjVx1MDAxZSVcdTAwMWGfqsBx4400XHTAZzZcbvXj9n2dveB+NfXd9Wp3s5x68FngXHUwMDFlXHUwMDEyXHUwMDAz6iWKr1x1MDAxNoHSXHUwMDFjXG5fekJQuyGi62x+pTn4SVE57eFAKTnhkM+kNVC6W4ODXHUwMDAzV90sXciGvpRPXHUwMDFke1W4Wlan2ldzfs3LOZNCJ8pcdTAwMWNI6FRcdTAwMDRHxiejSCek4VwiQeFQNLjSLmJcdTAwMDV3Q1wiVlxyZ4NcdTAwMDFwtiBcdTAwMTlcdTAwMWLVWDrC3cE4xVS4WFhPmFx1MDAwNTKc/Vqz//Kv/+0//ctf/s+//e+//Ntf/udCac5cdTAwMTiOXHUwMDEwpDnhxY7EYKx7REFsXHRcdTAwMDSNnlFOJChAXHUwMDFlXHUwMDFkUUorXGI1XHUwMDEznlx1MDAxNFJpjidOXHUwMDA2jVxmoJHplkktQFrF/VxyM2dfg6xcdTAwMWNjxmlr3lx1MDAxYtRFNXtX4CmpuWYgXHUwMDFjxVx1MDAxZWVcYqNcdTAwMDIsszRP56do0Nx8JPeV9mXnPtPdzZ7nZK+7v1c861W+rI9EcyaIoOJdk77Ck1x1MDAxNV93XFwhtDBcdTAwMDaB5lCCT1ntPDpQvzJU2qHBoFx1MDAxZMqUQM2idHhJX7uwI1x1MDAxM49cdTAwMTJ6hPHxecHf/H8ndsfw+Em5wKTiMkn/umhcdTAwMTCkXlJcbu5x6lxmgueKO1x1MDAxM2gpXHTaeFpcdEqapWbNep6jcieqylx1MDAwNIG4w0OwuODMXHUwMDAy2cp7o8aFUpQx6j26leTUYZt4XHUwMDA3jFx1MDAwM2lcdTAwMTDeXHRcIjejrcbUwo1qlyxcbjhcdIJcdTAwMTJcdTAwMTCHi6hQw3vOXHTtgFx1MDAxOUbTOPXc4Fx1MDAxNlx1MDAxMVxuNdRminFQjFx1MDAwYum4XGK10udMSmWZ+clcdTAwMTZcdTAwMDNzoyFcdTAwMGaPhcvdq8tcXKXH3lx1MDAxZVeLmcLR87lKXHUwMDE3XHKZWOlcdTAwMTNccqG6Ndpwwbk04VBcclJcXG244ZymSmql9JREZLSZXHUwMDEyIFwifJCCR11EyU1cdTAwMWZcdTAwMWVcdTAwMWSg0bilXHUwMDE2185cYsO1iZit+ZWYydCrQ/iYXHUwMDExXHJRsfNGUGIqKVx1MDAxZCSooIvGQOrForTU4S5GLOK/USxKaidlkXSr+SVrm1xiXHUwMDEyXHUwMDEy0dhaXHUwMDFipYBGvC8tXHT51nwp9Hqdv//WvG20bmulb81K6e/+4VvzrtBEOlGLXHUwMDE5XHUwMDAwNHVr6zEsZYyuj2t4PeYrTMdilIl1cSrOLUpcdTAwMDc7uYtzdH5mWtGqmPS0prg49ZpnKlx1MDAxOEVcdTAwMDKPSzTnXHUwMDA0XHUwMDFhXHUwMDE1XHUwMDFjuF4oiUEz2Vx1MDAxYrRBUiim0Z5cdGGXMiAsYz+Zpz2/dJPGQ/b5pZQ5qFx1MDAxZOq6avE31W2vpovDJHClSMnQqFx1MDAxN2CcpIlcdTAwMDRhXHUwMDBloz2yLaXlyHYtslx1MDAxOTdcdTAwMWSFSeJLkVx1MDAwMsU3nk+tOFx1MDAwM1x1MDAxMzH96Gs7U4ZeXHUwMDFkQsOMKFx1MDAwYqj4RFx1MDAxNqR9WtPQrYmlYPSZT71cdTAwMTTkxoN4KYg7r6lvXCJcdTAwMWG1gtpezU1cYsqISLpcZo9BQ2lM/7NlXGbzvE+0qESzXHUwMDEyXHRDr59cdTAwMTkpXHUwMDE5o7uDpORjjdPRXHUwMDBlYCY+e4WRPWJMgi7Zo3NcdTAwMGZSXHUwMDBiOZBcdTAwMWUorlx1MDAxY6DB4JxcdTAwMWSGXHUwMDFj6nVP8Vx1MDAwNVSJMc9cdTAwMDJcdTAwMDKeXHUwMDE5ZVxyMGFYXHUwMDA0XHUwMDAyXHJyJIFCQVxumtKq/D27f8RdXHUwMDA1IFx1MDAwYmGJjIhcdTAwMDVcdTAwMTKR4/5h/fVolVx1MDAxZKxellvOls169+YsXUQkgTNFqsFcZj4hXHUwMDA301x1MDAwNpVcZlx1MDAwZlx1MDAxNFxia/2ZO0+kQlx1MDAwM1x1MDAxNzUjnlx0aZW1YeqhvHBcdTAwMWbwL0U+YpFBj1x1MDAxMCZmxEa4jo3jkCOK42claIFcdTAwMTJ98FMvXHUwMDFhXHUwMDE196jvvtac6mjEcMRbO7TYXGbiXHUwMDExYUntkuc3XHUwMDFhLMqDXHUwMDEyXHUwMDFlQ1x1MDAwNMZcdTAwMWGVsMn2VyMjXHUwMDEzuUymXHUwMDFlQzSGnIzR73HkZEY+XHUwMDEyIeJ7XHUwMDEyaWu4XHUwMDA0maDb7ehGKmlcdTAwMDWkcM5DMeeMRZ5ipFx1MDAxNsFcdTAwMWNcdTAwMTSFlplCnuJcdTAwMTC0flx0Nnu2wlxmp9p6XHUwMDAxVnDOjW/rfW5cdTAwMTPmoZlO1ImyYkw4+Fx1MDAwM1x1MDAxMt+NZjPMvVx1MDAxM+d0fOV041H29y9LN73NbFY+qNvjR1tKXHUwMDE3X0ngOOE0+VxisYJ2+2AmTJiugFx1MDAwN85cdTAwMDF37+4spZe1UGfE8aVH+ODOiFS4XHUwMDEx/T5cdTAwMTFcdTAwMGJcdTAwMDZ3fHJcdTAwMTFcdTAwMTZ9ONMuwiRcdTAwMTeeNXi+gFx1MDAxYmb9k4nfPVx1MDAxY9ZjXGZcdTAwMDW54zREhcH8/LwyXCJrLuzioI5q0l9itTycYjA2cGXhLo4xmjfIXCJ8q5yON5j4hCzlXHUwMDA2zis9eWxldFx1MDAxOURaMUe0XHUwMDAx+bvVQL1cdTAwMDG0XHKU6FCGXGJcdTAwMTfaOs4tvkrNXHUwMDExdDFcdTAwMTlcImjaOTz61JkoPKncoHGrRVrTVHvt8nWvZ3ry9mQrv7fb2bnLtVi6KEJcdTAwMDKXXHUwMDA2x9Mh+WBcdTAwMWWc44KxqOGEhubTXG5cdNTxXHUwMDE1jeDwzs7cx0EjXHUwMDFjnNJGMTzBVvN/d1x1MDAxOVwifnzMiorEjzRcdTAwMDA0XHUwMDFkjFx1MDAxMGJy90Y0XGLSLlx1MDAxNpFmeDJeLFwi//NotprWxikjYK4tXifhXCKISmWYMcuYplrpdfrNXCIu4Vx1MDAwZpXSX1x1MDAwZiaIv9Wis1bnxUvGqPYgL4le8HRcdTAwMTSFx888XHUwMDA3prVWSiWoXCJcdTAwMWXdpyylYJTW44o7roBcdTAwMDaWWjbcWZO6jzBUS1x1MDAxNlx1MDAxOIBcdTAwMTNsju2WI9M/wFOOXHUwMDE2xiz+XHUwMDA3XCLcboScMWgsptSNcXCTa1dP+vd6w3VuxYbsb5bvUsZRJnZjUF2HptJcdTAwMTVcdTAwMTTXSD9o9mWIXHJcdTAwMThPIFx1MDAwZlBqMClSXG4772Ka9yXhXHUwMDA2OUu5lUqFXHRcbvdcdTAwMDBVjNGGZqgoob54SMb/6jA8ZkRRIDaHXHUwMDE1tbbkuOWTM5RoXGKkXFwoXG5cdTAwMTgpXHUwMDE0NbJefIKm73Jka3OsOFx1MDAxNFx1MDAxMd7diDpcdTAwMWFk6SiapVxcxlx1MDAxNNZKq3NcdTAwMWbJR+ZVRTNGjYf4XGKtbzr6oX1z8Vx1MDAwMkBDIcqss1x0csVHN3RIKdCU8SSg5Kb2Z5Y5XHUwMDE47n0mXHUwMDFjWWCKS2O4UdzMcVx1MDAwZVFcdTAwMDT7QEucmo470JRhKmSokJc7oWhkbUrJx/7dzePBaX9rr57Reye5K8iZp9t0kY8kmZ7KclThtOM09JyHcz6kxzlN1LXKOLxjXCI8XCJ4tv5cdTAwMTFakpOMXHUwMDAzXHUwMDE1MKJcIohYkqHRVlx1MDAxNi1cdTAwMWLFlKSA/lx1MDAxMrGPXHUwMDEwPGZEPnzej4j+6cxpLj+l5tjZqpFcdTAwMThIuVBcdTAwMDTruXihaJj2jKOeI8CNlXKx9TNcdTAwMTGpb4ZcdTAwMDZYM8eXlXxMlPrhm0gxUzIyRqtHkZFcdTAwMTmlfUB8XHUwMDFlXHUwMDE2zStHSZykNWu9VGu/XHUwMDFllyr941xcvaBP7VFDZvbTXHUwMDBlxIxmytPUx5hTXHUwMDE3XHR/O0e6XHUwMDAwTSVB2S4k2l9oLrB5TjKQzONglZaKcc51hNtSeVx1MDAxNo1cdTAwMTFkjFYyNHd9vZs+rFx1MDAwNDRcdTAwMTBcdTAwMTj1nkgnX6k91vdcdTAwMTh/7eS28iWTv7/f3np8rP1cdTAwMWG+XHUwMDEyP9r1y2ZexFx1MDAxZiF6ZEKnZ0ba3MVcblx1MDAxMcpAYtSOc2JcdTAwMTlcdTAwMTJ9QNIuQyiPXSlJfZHp+4qAXGZBXHSjNN5cdTAwMTSH90daPsdJq74oy6hcdTAwMWWpYC0uJVFcdTAwMDPnr6LNqXT05lx1MDAxN/TlXHUwMDE4o/yCSty/zOl0t1x1MDAxMLGOXHUwMDA156y0eCAn9+A9871qvVO4f8yL/Oa6fT1Yz9VSXHUwMDBmO8Wdp1x1MDAwNVD+ukRb0Fxye/BQo3vKaFx1MDAwMGdccmpxlI1zg1x1MDAxZFx1MDAxN9YzXHUwMDE2wWdcdTAwMDUyd8ajWqVK7eHL0LzVXG5NSmChzqmcOeu0Y5DSXFyMN2WubLP2Um9u5c9WX2R/+7yYsnTNt6eb1tvbsZTlq3J1/fJUXFx0IVx1MDAxN23W46Ehg95cdTAwMWHqxqHgs7h45bNZXHUwMDA3XG5zfFx02WFOoJlcdTAwMTa265eDNWRcdTAwMDCEp4EpgbpJXG7Hmfa/X0rvPWuVWUncQo69XjxcdTAwMWNcdTAwMDbXXHUwMDBiXHUwMDAyYUYsXHUwMDA0ID6FXHUwMDFkv1x1MDAxM9XUJqjzjz7tKVx1MDAxN4jCUCRMIPezyiom5bBEVHifhdR4qC2gduDzy0SzXHUwMDExXHUwMDEz3sMpXHUwMDE3XHUwMDA2RZ50OpFv9SvRkFx1MDAxNWph8W6hr8S4XHUwMDE05pVvMUafR7GRwGqnIyXAY5MtUFtTekFcdTAwMDLHXv7q4PS5fNh7PttF6NrLm/Lr+XraMZhRXHUwMDAwnuDKacEtXHUwMDAw12a4jERa5iFdUUJIo1x1MDAxZMyxP1x1MDAxZiAlwo8yILnQXHUwMDE0aopgJSyISUB1h4rAQEo7hmmZvbg6tWuFY5blm7vPpcrp42XaXHUwMDFjXGI/zU8yzGNcXHEhXHUwMDE5aFx1MDAwNpyD9FxyQV/5rD1cdTAwMTUkzCXNo1I0petrR1x1MDAxZeLPa+DtP0dcdTAwMTPiq0SY00ZwlsBbXHUwMDExfVx1MDAxYdMuoYT2cHuNpmReolx0w5mZUplcdTAwMDVcdChcdTAwMTeR+Fx1MDAxMGZcdFx1MDAxY1x1MDAwZoJcdTAwMDPywS4pTbh5r+X8roNcdTAwMTdKXHUwMDEzxmjYSKfF8Gqno1x03MSOg1NWXCKBXHUwMDE1dnKa8FpsnNdcdTAwMWYzV5uZI1bZ2zjj7C4v01x1MDAwZcLRvlx1MDAwYqVcdTAwMDRBVGq0uaQ0Zn5MnaPMZY4sP8CbXCKZjmLu4Fx1MDAxOarIN1x1MDAxMoVcdTAwMDKzXHUwMDEx3fVcdTAwMWNcdTAwMTE7ltKYw876xeaaOdtltre6197Sr93iYfPLOi7wrCjqkMDAXGZaXHUwMDA3WN+r/t05LjjzJFx1MDAxNW9cdTAwMThcdTAwMDFUls2t//1cdTAwMTRcdTAwMGJcdTAwMTGG8kZpUptcdTAwMDSmx14wXHUwMDFlXHKDXHUwMDBiXHUwMDA2cTAjSlx1MDAxMt9OlJbB8DYn6Fx1MDAxM1x1MDAxNH3aUy5ccoWhNsrWclx1MDAxNHigXHUwMDAz0zGVU1x1MDAxZUilQChlwcnFui1cIjIxJXNIkpazo/lod8W8gidjVHiQh/y0l1wiPnRcdTAwMDJUOm2t1JPzj8b1Q11cdTAwMTdeTm3rXCIrXHUwMDFhlYtK29Z7aUdcXEbQ5Dg0MUE5gVx1MDAwNMRcZocskXx4klM8XHUwMDE52bdcdTAwMTVufmlcdTAwMGaOozWCstYpXHUwMDA2UoKLyEdcbjspuFBOg0lpUubL9WE529y3Z7mNnfu9t+deXZw/LKOPXHUwMDAylKCJK0JcdTAwMDNX1rjwlJNhXHUwMDBmhVDhWpGvxDdij2rg3T9FXHUwMDA2RHy1XHUwMDFhKkItaVx1MDAxY/vEoin6JKZcXDRx8Fx1MDAxNKDMkVx1MDAwNjHOKFA3LJq4WZhoirCFwv5cdCqYUo4vrXti0CTiV1x1MDAwNTLGKNdIXHUwMDBmRWjB07FcdTAwMDTOY3k5ICNnqDxcdTAwMTNcdTAwMTSOVnbvSme58rY5O12rvpZcdTAwMWav9zpVSDtcdTAwMTRcdGs0wFx0xVx1MDAxZFx1MDAxYZ3+vvqDXHUwMDFlddx6hlx1MDAxOaqbN2hhwfyQiHacp1x1MDAxNY01MqCU1DaCJeBLXHUwMDA0mnm4XFy035xhXHUwMDEwquWgXHUwMDAyXHUwMDEzJiFRXHUwMDEy81x1MDAwMmnD7sNzx+28rTUtPymcP7rTri3+otzImVx1MDAwNDCMXHUwMDEwaJRrXHUwMDAxQjvuXCLGoShcdTAwMGbVmVx1MDAwNmtccrFcdTAwMDQzblx1MDAxYcpcdTAwMTf2U1x1MDAwMHhcZlxmt5q6IdDsXHUwMDE3//vx4HLHpFx1MDAwNUXD1ECrsdeLh8P361x1MDAwNYAwI2qi460mXHUwMDA3+Fx1MDAwNUSSXHUwMDA2W9GnPeXyUFx1MDAxOOEhjaVcdTAwMTbhlO05TEw0Psm0Qy4w8GDD/Fx1MDAxYYibiebEXHUwMDFhwbVcdTAwMDJul5GYXGZUfJfeXHUwMDE1RUbm5aZcdTAwMTijwyO7a1xyXHUwMDE2OVx1MDAxZFx1MDAwMZHxLftcdTAwMTXNQmAmQYZnNlx1MDAwZoXr++rTdq9T3l039dP2WvZcdTAwMGLMZmbOQ7liLbOOM8LdXHUwMDEw5PBcbp5VXG6lJ1x1MDAxN1xuaJLL3DBcdTAwMTfZukJ6jnFcdTAwMDaAUptcZrOo9lp4m5JcdTAwMDBwgXxjXHUwMDAzVC6f3zSX5XzGrrVcdTAwMWGX6/s3v8hNMYP+m8xq/F86WpPhmvuSXHUwMDA3PtpYceaQrtK4UutAjlx0i8yinpVZITRll2o8oaiwwzlcdTAwMWNLNbokXGKHXHUwMDE58Vx1MDAwZYiXg0wo7ZyAyXlH9KlPvVx1MDAxY1x1MDAxNDSDPl5cdTAwMGUq5Vx05Fx1MDAxZZRCLpB2mzlcdTAwMGUv+fxcdTAwMDajUjupqlWCWsZcbpNcdTAwMGVcdTAwMDJr4Y09x6jvIPX4WON0zMM/sS9cdTAwMDA5S3VlTCfoXHUwMDA2vp/dxb0/Ws9cdTAwMWX14fqsvXnUrOSO0o84pjwwXHUwMDE2jDBWWlx1MDAxNihcdTAwMGJcdTAwMTXWedqhlSWtRbFcdTAwMGV6oV0ruNCepNg+XG4/5D3Khrg/XG5cbkpcdTAwMWXhc4+QfInOXHUwMDEy5IxAZqCYomBcdTAwMTdxhKjWm9Rzk0aWU4Bf84hkyuXwRlxmvTp8klx1MDAxMursXHUwMDExMdZ476lCs1Rqm8B72uJcdTAwMDcv4EyPufq5al3uXFyd9m3qc7zGyFx1MDAxMM2RLyExVMJcImvye5tnntcgpGc5TVx1MDAxM+FcdTAwMGVNkk9pMVwixKpcdTAwMDZeXHUwMDBlkVZnaS1z299kxcODUvHqdPVoXHUwMDE3OaB1y1x1MDAxOGPlXHUwMDBlaFBcdTAwMThcdTAwMTiFXCJdhdvPXGJcdTAwMGZcdTAwMTmfQouGMzxHXGaNn9CWfyXpXHUwMDE0d1RcdTAwMDNvnkg2TWFPXGZ2mlLKJpZM0Vx1MDAwNzHlkkk7bzBTS1ltXHKoQPJcdTAwMDfqwFx1MDAwNVx0Jlx1MDAxN0FuXCImIaK1jurJJapJ+SrWxCBi+U7Xf02EdYxqjYywXHUwMDA21ztcdTAwMWRF4DzWykBCwpFdJ2g+c3djqi93XHUwMDBm7bX6Rb5Run+uXHUwMDFmvFRcdTAwMWbTjkPUr1x1MDAxZVpcdTAwMTCCXHUwMDBi4Vxm+DtDvM9cdTAwMDBD4Vx1MDAwZZLyXHJcdFx1MDAwMnOcSDpJXHUwMDAxOy7Vacm50VQ3zYZcdTAwMDYgfW/UXHUwMDBiXFw7I1xcSinDcb78sG6fL13zscLMib3tXHUwMDFkX6dsUHNcImJcdTAwMDBk01AwSjFUlS7s8Fx1MDAwNI85xZijXHUwMDA0LTVcdTAwMTirXHUwMDE32tqJ6IraOrl6y2aq2atrtXet8yZv9uPGR9NIXHUwMDE3bqjjgjRaQXhR3MOtdFZcdTAwMDHazYy6MnxpujJIQXdMXG49XHUwMDE4UlxybrjlXHUwMDBlYkY4zbk2jp7mduz1RtfOhzE4I0ok41x1MDAxYvJcdTAwMTmajGBYgtr5aKClXFxcdTAwMTRTPqwmX1x1MDAwZVxy8kL7X1xye1hccqeyXUdcdGdgrVx1MDAxM/NLQlx1MDAxN1x1MDAxMbktXHUwMDExnIisdq6SXHUwMDE13XxcdTAwMTlO1K98a8ZGd+dFhMYwiFx1MDAxMFx1MDAxMfpY5HTsJ37iXCKnXHUwMDAxciZBTOOSXe2UWyfQ7Lzt7PeuMlx1MDAwZle3t6lcdTAwMWZKMCi2cUpzI7lcdTAwMTVANX9DiHPkYVx1MDAwNZR1yFx1MDAwMrmbY5onymjq8iuUXHUwMDEyWjCtRERcdTAwMGYtUMIzwlxiISnlmWtcYuKRRrhy/DIpTUnfeOq7K3PduqjvltlWJvu0d1HdSVx1MDAxN/dJMqSAobRcdTAwMDbBKM9cdTAwMDZcdTAwMGaP4Vwi7Fx1MDAxN1FcdTAwMWX1P2HUQlXindNTTotO4mOWaKPiObBAjjNfLpSP+1xiaaxcIptbXHUwMDEzyL/2IKV43NAjhJhcdTAwMTkxlfi5XHUwMDA1aCBZpjRLkFx1MDAxZlx1MDAxZlxyi9RcdTAwMGJOzVx1MDAwN4WIjmZ2XHTEQDBcdTAwMThMxcPOOmlcdTAwMDZcdTAwMWRIzPxcdTAwMWGf+Vx1MDAxYZCO4CraoFx1MDAwMVwi7VK2XHUwMDFiLDZa3fLCo8FjXHUwMDE0fpCqfC5yOqriYq1cdTAwMDNh0PI0MkF9arW9cVx1MDAwMnuP9Z7ZrfDrrJFcdTAwMWL5VZ5+yFxyLC/LgVx1MDAwNuFxZYczP4WjXHRKaGJbKaxTc2zwXHUwMDE54S81TKE0kINOQ1xmXCLS3qmNM6qdlLZcdTAwMTXs1mz9otjZXHUwMDE35qqfe3pa48dr7fN0MZNEXHUwMDE5X6iA0Di3nFHmrdZRI1xmJM1cdTAwMTdH5utw9dyOXHUwMDE5XHUwMDAzPYNcdTAwMTFcdTAwMDZMOcaBXHRcdKiwqVYpYjj1l2ZcIkOvXHUwMDBl4WFGxCO+XHUwMDFhiIpoXHUwMDE5c2Zygy360KdeXGIqPUJcYlwi1fMk56CBelx1MDAwYs9xjEtcdTAwMTTpiKjS59RcdTAwMWaDLed06YFCX2jq+1x1MDAxOLVcdTAwMWTJOKZlXHUwMDFi8VxmX3LNnJJcdEaTmXyl19xnjfv7w+yWOLvsXHUwMDFk5V6/XHUwMDAy3Vx1MDAwMM8yslwirVRS8mDmiNWeprBcdTAwMTF3RnCh5udcdTAwMWIxaGpYXHUwMDBiVllj0bQ1XHUwMDEx0Fx1MDAwYqeOSFx1MDAxYatmpU5pXHUwMDBiwZ3CXHUwMDFkk4f1mro5K92eXHUwMDFlrsudXFxud1x1MDAxOVNHJJNcdTAwMGU1v0DNb9TQMIBcdTAwMGZ/XHUwMDA02o6Ur+3AMrxhX7s8P/asXHUwMDA23v1TLMDEXHUwMDA3SqRcdTAwMTbg0OhO0Ksn8iSmXFw40bw2YFRcdTAwMTBM81x1MDAwZUBcdTAwMDZEk+JcdTAwMWUsRDRcdTAwMDFMNrrI0TRcdTAwMTQpl7JcZu59isC7dT/4uVQu3nTKlXG5JPOaZDRG20ZcdTAwMGZBXHUwMDE4vfwpOUR8qapcdTAwMDI6XHUwMDEyPEHy6b3JZjN7W7Wj3nEm086p2mVmvZJ2mGZogqmyoIVcdTAwMTFSXHUwMDE460vHjYiu6Pn5XGIjXHUwMDBi51DzXHUwMDE4rlx1MDAxNdXH2ahZXGLE8VRcIuq+QFx1MDAwMvEkUPplntTjVr8jTzo35+VcdTAwMWLVTpfLXCJJMIVcdTAwMTJcdTAwMWVcdTAwMTVnnDk07KyK6uIzXHUwMDE0SmFTXHUwMDBlXUxcdTAwMTBKXHUwMDAxo5GyXGZcdTAwMTS5oEZP4TRcdTAwMTLhKeBKXGItrVx1MDAxNFor+7V7XHUwMDFmXHUwMDA3XG7pXHUwMDAy8JhcdTAwMTF34fGtj7WhiFx1MDAwMfDJQyfRIEi9UHTCY7FCXHUwMDExtPSkodnmlI/nXHUwMDBiq8xaJqqIXHUwMDE0/LBcdTAwMGKD2kw5WspcdTAwMTIyl35z8Fx0UeRkXi6MMWo8SE6+r3A6/mFiocaBMypE4pM3XHUwMDE4LL1aWbu4q/b5w7Hq2Ivt+yuR/lx1MDAwMjrQ4Dk0XHUwMDAzXHUwMDA0o3buXHUwMDFjXHUwMDAyXHRVoVxuukVcdTAwMTJcdTAwMTDDrFx1MDAwN1xcSMvQOEY7JZTOgcqQS1x1MDAwMEjU1WuBXHUwMDE05LLRzJYq+fJTX+eu83l+eL7W6KeLgiSJmtDUXz4oQVx1MDAxM8ZwXHUwMDA148rzSDtOR0JcdTAwMTKETagxkaW5n6iCmeYgQotaqqhJXHUwMDAwXHUwMDExM+JcdTAwMWNcdTAwMTBviHGkk8jzXHUwMDEzxI6jT33qXHUwMDA1obVcdTAwMTRcdTAwMTiJXHUwMDEzhLjZXHUwMDFl1TpYhVx1MDAxYlx1MDAwZnaOvd4nY1x1MDAxZFx1MDAwMJZbXG5cdTAwMTQuY2bpL6BcdTAwMWRjtPdMaVx1MDAwN4xcdTAwMTis7piwXHUwMDE0ppzc78H4a+3l7bqzW6uf1Lb3WXfjXHUwMDFjRPrhpplnlEXjkHpcdTAwMTYyf0+yXHUwMDAx3IzyOFwiTVmG6p3NsbhNW0NDXHUwMDFjrEZcdTAwMWWhXHUwMDE0RM1zXHUwMDBlx040mruk5lJaQ/OyebZeXFw/3z09Xd14bbTXumj5XS1j7ERpO8jnxIMkrc+N/aH7tWdcdTAwMWNDrqLxQHGO9/pLc4HYs1x1MDAxYXj3z3GBXHUwMDExhbdCcCqRT9LbOPIoplxcOuHueqCoXHUwMDFiXHUwMDA1p0ntPOCAQIR4ZiHCXHRkhFlcdTAwMTRcdTAwMTE9QbxcbraUk1x1MDAwZb5cdTAwMDdcdTAwMWPoiH2MM/rWfFe/f6hVVjrlyt//PfvrhcZPxmjcyPjJXHUwMDA0X2FKMlx1MDAxMd9cdTAwMDSI+lx1MDAwMUtAW2HylCedWd94vilmT81LM18pu91cdTAwMWJ1e512uCrlISVnNEFcdTAwMWFVMpfDXHUwMDE5T45bslxcQVLZn+BqjilPXHUwMDA2qIVcclxuaMecUf5cdTAwMTFMI7iEM466MsuUZn5cdTAwMWVnrlx1MDAxZuulw93My1Nzbbt2d5bdvDpbRi5hKeGTO+e0klx1MDAxYXg4wLJcXFxcXCL+sFx1MDAwNt7+k2RCXHUwMDA3f/uRlM5gkF07sWyKPokpl01cXFxij8LYnFO/XHUwMDE0o4aHJjlqPbRcdTAwMTDZ5J+7PapgVVBXSMtcdTAwMTONt/9SXFyiUCr5k1x1MDAxN25cdTAwMTbcx2OMgo3kXHUwMDBlXHUwMDExS56OK4zwO5BcdTAwMDUlwIjJ3XxmdW39qde/7XdUoZNpNPM3V5X052xSUZbhyOmBKUHzS4bxaD0qyVx1MDAxYcxd5cKaXHUwMDA1NyrWntVaolxcxL9cZlx1MDAxN6GEXHUwMDBilKaShu6KlMY7XG7d51x1MDAwN3exsdO/7ubE48XuSaW9+ZaueEeSlFx1MDAwYqGFXHUwMDEzSM9cdTAwMTij4lx1MDAxYyPD7oX3IY7c6UEpXHUwMDAzMojQzs4650JoabSx2lxuoFmeXCKin8iXpiWBXHUwMDE0iyBcIiakJaOO46dcXI/kLlx1MDAwMylh3JCUXHUwMDE49k1SXHUwMDA3ZSY4UFxyXHUwMDExhUZ9Re9cdTAwMWbdIUbPTFx1MDAwZcumWYmXeUvdz1x1MDAxNUdL3YRk4UOu0ZBUf4MzXHUwMDFmg1x1MDAxY9HzhFp2XHUwMDBimGhcdTAwMTJ4UMJcci1jhmLoXHUwMDA31yj128P1polcdTAwMTnL9JQjmsZcdTAwMDSTQEer7yBcdTAwMTH58X2mI1x1MDAxZSNmklD1M+WrTVx1MDAxZfDobVQzXHUwMDE2Topqc+dZq4P8lqpAPv3EXHUwMDAzhYZjNIBcdOVcdTAwMTmamcE8XHUwMDBi69H8JoH6nWuhXHUwMDE2SjxcZjOeNVx1MDAxYdlcdTAwMDW1K3JcdTAwMTFFWoyaXHUwMDE4gUxppicrVjvrN1x1MDAxYvVC5+34dXt/r3X/mLaWYUnSLPBcdTAwMDRcYqOtRmNRWDRcZsNcdTAwMWGeklx1MDAxZdhgljKz1sK40dEzyLJcdTAwMTBcdTAwMWHpj8BtYpJcdTAwMWFvmojaVM9IytpG1oxmLv70tctUXHUwMDAyaVx1MDAxN8NcdTAwMDCZkIWMXHUwMDFkXCJcdTAwMTlbQidcdTAwMWO1VbVcdIRiNFx1MDAwNtIuXHUwMDE0kV6hwVx1MDAxNStcdTAwMTVcdTAwMDfpLopafaFQ1HaOXHUwMDE1+zZcIulcIlx1MDAxY2cxNGWPa7c458jnJ83bOTJQ8M3y80ql9Hf/8MPVsODClDGaPZKTRCx5Oo7iO3tBNEokmoYlqGe9OKlcdTAwMWZtXHUwMDFjV+qsVNuD4lbz5FVyk3o0jvaNWGU8XHUwMDA0qFx1MDAxNFxuXHRcbpq781x1MDAwM6NyXGJ7qlx1MDAxObBCcKVMhDFcdTAwMTVcbqNoXHUwMDFho0fey3RSXHUwMDE0Xa1WL9u3+cxZ8fDycOftRVx1MDAxZaxuJqcoU4mDiaMoX3ZYQuyBXHS8e07aXHUwMDFhLCkpJvTkaVx1MDAxMdHnIeVcdTAwMDJcdTAwMDKM8qTlxlo96FIyXHUwMDFjZ6We8IuRXHUwMDBmk8YyrFx1MDAwMHLjLnDm8+LUdVx1MDAxYWJcdTAwMTljtFxcsljGb9/R/Xuh3T7t4d5+XGLo359q5ee18Fx1MDAxOfmryuBB71x1MDAxZmCZUFNcdTAwMWXI9T//9uf/XHUwMDA3k7imNCJ9 - RPC ServerDeamon(Primary metadata owner)blob_handle:spdk_blob*blob_idref_count...handleidspdk_thread_groupPasermd threadsio threadcreatedeleteopencloseresizereadwriteref++ref--NVmeOther Process(Secondary)Main ProcessHOOK目录操作Linux文件系统createcreatexattr=blobidfd<>handleidopenfdopenfdfd<>handleidwrite fdftruncate(fd)reszieforkforkfd<>handleidblob_createblob idhandle idblob_openblob idhandle idblob_writehandle idwritesizereadfdblob_readhandle idbufsizeclosefdcloseblob_closeblob_dec_refhandle idunlinkunlinkblob_deleteblob idunlink(if ref==0)blob_add_refhandle_iddupnew fd<>handleidblob_add_refhandle_id \ No newline at end of file + RPC ServerDeamon(Primary metadata owner)blob_handle:spdk_blob*blob_idref_count...handleidspdk_thread_groupPasermd threadsio threadcreatedeleteopencloseresizereadwriteref++ref--NVmeOther Process(Secondary)Application ( Postgresql / other DB)HOOK目录操作Linux文件系统createcreatexattr=blobidfd<>handleidopenfdopenfdfd<>handleidwrite fdftruncate(fd)reszieforkforkfd<>handleidblob_createblob idhandle idblob_openblob idhandle idblob_writehandle idwritesizereadfdblob_readhandle idbufsizeclosefdcloseblob_closeblob_dec_refhandle idunlinkunlinkblob_deleteblob idunlink(if ref==0)blob_add_refhandle_iddupfddupnew fd<>handleidblob_add_refhandle_id \ No newline at end of file