geopro/tests/render/test_async_region_builder.cpp

342 lines
13 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// AsyncRegionBuilder(C3) headless 测试(真线程,无需 GPU
// 后台 worker 调公共重组核 reorganizeRegion 从分块 store 重组 RegionTarget→单张
// VTK_SHORT image主线程非阻塞 takeLatest 取最新就绪。
//
// 验:就绪内容 == 同步重组逐体素一致 / supersede 收敛最新 / 析构忙时干净 join /
// 并发数百次不崩不死锁 / takeLatest 非阻塞。
#include "render/source/AsyncRegionBuilder.hpp"
#include "render/source/RegionReorganizer.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include <vtkImageData.h>
#include <vtkPointData.h>
#include <vtkShortArray.h>
#include <chrono>
#include <filesystem>
#include <thread>
#include <gtest/gtest.h>
using namespace geopro;
using geopro::render::AsyncRegionBuilder;
using geopro::render::RegionTarget;
using geopro::render::reorganizeRegion;
namespace {
// 造一个含金字塔的 store值 = 全局 (i+j+k)%1000便于校验块定位非 64 整除
// 维度以含边缘块。返回 store 目录。与 C2 测试同口径。
std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz,
int brick, int levels) {
std::filesystem::remove_all(dir);
core::BuiltI16 b;
b.vol = core::ScalarVolumeI16(nx, ny, nz);
for (int k = 0; k < nz; ++k)
for (int j = 0; j < ny; ++j)
for (int i = 0; i < nx; ++i)
b.vol.at(i, j, k) = static_cast<short>((i + j + k) % 1000);
b.quant = {1.0, 0.0};
b.origin = {{1, 2, 3}};
b.spacing = {{0.5, 0.5, 0.2}};
b.vminPhys = 0;
b.vmaxPhys = 1000;
data::ChunkedVolumeStore::write(dir, b, brick);
{
data::ChunkedVolumeStore s(dir);
s.buildPyramid(levels);
}
return dir;
}
void sleepMs(int ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
// 轮询等就绪(有超时上限,避免卡死)。
vtkSmartPointer<vtkImageData> waitReady(AsyncRegionBuilder& b, int maxTries = 500,
int stepMs = 5) {
vtkSmartPointer<vtkImageData> img;
for (int i = 0; i < maxTries && img == nullptr; ++i) {
img = b.takeLatest();
if (img == nullptr) sleepMs(stepMs);
}
return img;
}
// 两图逐体素相等dims/类型/标量)。
bool imagesEqual(vtkImageData* a, vtkImageData* b) {
if (a == nullptr || b == nullptr) return false;
int da[3], db[3];
a->GetDimensions(da);
b->GetDimensions(db);
if (da[0] != db[0] || da[1] != db[1] || da[2] != db[2]) return false;
auto* aa = vtkShortArray::SafeDownCast(a->GetPointData()->GetScalars());
auto* ba = vtkShortArray::SafeDownCast(b->GetPointData()->GetScalars());
if (aa == nullptr || ba == nullptr) return false;
const vtkIdType n = aa->GetNumberOfTuples();
if (n != ba->GetNumberOfTuples()) return false;
for (vtkIdType i = 0; i < n; ++i)
if (aa->GetValue(i) != ba->GetValue(i)) return false;
return true;
}
RegionTarget makeTarget(int level, int bx0, int bx1, int by0, int by1, int bz0,
int bz1, double exagg = 1.0) {
RegionTarget t{};
t.level = level;
t.bx0 = bx0;
t.bx1 = bx1;
t.by0 = by0;
t.by1 = by1;
t.bz0 = bz0;
t.bz1 = bz1;
t.exagg = exagg;
return t;
}
} // namespace
// ── 就绪内容 == 同步重组逐体素一致 ─────────────────────────────────────────
TEST(AsyncRegionBuilder, ReconstructsRequestedTargetMatchesSync) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_match").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
const RegionTarget t = makeTarget(1, 0, 2, 0, 1, 0, 1, 1.0);
// 同步参考:同一重组核对同一 target。
data::ChunkedVolumeStore refStore(dir);
vtkSmartPointer<vtkImageData> sync = reorganizeRegion(refStore, t, 16384);
ASSERT_NE(sync.Get(), nullptr);
AsyncRegionBuilder b(dir);
b.requestTarget(t);
vtkSmartPointer<vtkImageData> async = waitReady(b);
ASSERT_NE(async.Get(), nullptr);
EXPECT_EQ(async->GetScalarType(), VTK_SHORT);
EXPECT_TRUE(imagesEqual(async.Get(), sync.Get()));
}
// ── supersede连发多个不同 target最终收敛到最后一个不崩不死锁 ────────────
TEST(AsyncRegionBuilder, SupersedesStaleRequests) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_super").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
AsyncRegionBuilder b(dir);
// 连发若干不同 target最后一个为期望最终态。
std::vector<RegionTarget> seq = {
makeTarget(2, 0, 1, 0, 1, 0, 1),
makeTarget(1, 0, 2, 0, 1, 0, 1),
makeTarget(0, 0, 1, 0, 1, 0, 1),
makeTarget(1, 1, 3, 0, 2, 0, 1), // 最终态
};
const RegionTarget last = seq.back();
for (const auto& t : seq) b.requestTarget(t);
// 同步参考 = 最后一个 target。
data::ChunkedVolumeStore refStore(dir);
vtkSmartPointer<vtkImageData> sync = reorganizeRegion(refStore, last, 16384);
ASSERT_NE(sync.Get(), nullptr);
// 轮询直到 pending 清空且取到与最终态一致的结果(收敛)。
vtkSmartPointer<vtkImageData> latest;
for (int i = 0; i < 800; ++i) {
auto img = b.takeLatest();
if (img != nullptr) latest = img;
if (latest != nullptr && !b.hasPending()) break;
sleepMs(5);
}
ASSERT_NE(latest.Get(), nullptr);
EXPECT_FALSE(b.hasPending());
EXPECT_TRUE(imagesEqual(latest.Get(), sync.Get()))
<< "最终就绪结果应收敛到最后一个 target";
}
// ── 析构忙时干净 join建后立即析构worker 在忙)应不崩不死锁不泄漏 ──────────
TEST(AsyncRegionBuilder, DestructorJoinsCleanly) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_dtor").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
for (int rep = 0; rep < 20; ++rep) {
AsyncRegionBuilder b(dir);
// 立刻发请求让 worker 忙起来,随即析构。
b.requestTarget(makeTarget(0, 0, 3, 0, 2, 0, 1));
b.requestTarget(makeTarget(1, 0, 2, 0, 1, 0, 1));
// 不等待,直接离开作用域析构(必须干净 join
}
SUCCEED(); // 到这里没死锁/崩溃即通过。
}
// ── 并发压力:主线程循环 requestTarget + takeLatest 数百次worker 并发,不崩 ──
TEST(AsyncRegionBuilder, ConcurrentStress) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_stress").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
AsyncRegionBuilder b(dir);
int gotCount = 0;
for (int i = 0; i < 400; ++i) {
const int lvl = i % 3;
const int bx1 = 1 + (i % 2);
b.requestTarget(makeTarget(lvl, 0, bx1, 0, 1, 0, 1));
auto img = b.takeLatest(); // 非阻塞
if (img != nullptr) ++gotCount;
}
// 排空:等到最后一批就绪。
auto fin = waitReady(b);
EXPECT_NE(fin.Get(), nullptr);
// 过程中至少取到过若干结果(功能层证明 worker 在产出)。
EXPECT_GE(gotCount, 0); // 不强求次数,关键是不崩不死锁
SUCCEED();
}
// ── takeLatest 非阻塞:无结果时立即返回 nullptr不等待 worker ───────────────
TEST(AsyncRegionBuilder, TakeLatestNonBlockingWhenEmpty) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_nb").string();
makePyramidStore(dir, 128, 64, 48, 64, 2);
AsyncRegionBuilder b(dir);
// 未发任何请求 → 立即 nullptr计时应极短
const auto t0 = std::chrono::steady_clock::now();
auto img = b.takeLatest();
const auto dtMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0)
.count();
EXPECT_EQ(img.Get(), nullptr);
EXPECT_LT(dtMs, 100); // 远小于任一次重组耗时,证明未阻塞等 worker
}
// ── C3-3轮询某 target 的 getReady 直到非空(带超时)──────────────────────
vtkSmartPointer<vtkImageData> waitGetReady(AsyncRegionBuilder& b,
const RegionTarget& t,
int maxTries = 800, int stepMs = 5) {
vtkSmartPointer<vtkImageData> img;
int lv = -1;
for (int i = 0; i < maxTries && img == nullptr; ++i) {
img = b.getReady(t, lv);
if (img == nullptr) sleepMs(stepMs);
}
return img;
}
// ── C3-3预取的目标建好后即刻命中无需再等─────────────────────────────
TEST(AsyncRegionBuilder, PrefetchedTargetReadyImmediately) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_prefetch").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
const RegionTarget cur = makeTarget(1, 0, 2, 0, 1, 0, 1);
const RegionTarget nxt = makeTarget(1, 1, 3, 0, 2, 0, 1);
AsyncRegionBuilder b(dir);
b.requestTarget(cur);
b.prefetch({nxt});
// 等两者都进缓存就绪。
ASSERT_NE(waitGetReady(b, cur).Get(), nullptr);
ASSERT_NE(waitGetReady(b, nxt).Get(), nullptr);
// 此后 getReady(nxt) 即刻命中(无需再等;计时极短)。
const auto t0 = std::chrono::steady_clock::now();
int lv = -1;
auto hit = b.getReady(nxt, lv);
const auto dtMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0)
.count();
ASSERT_NE(hit.Get(), nullptr);
EXPECT_EQ(lv, nxt.level);
EXPECT_LT(dtMs, 50);
// 内容 == 同步重组(缓存不改内容)。
data::ChunkedVolumeStore refStore(dir);
auto sync = reorganizeRegion(refStore, nxt, 16384);
ASSERT_NE(sync.Get(), nullptr);
EXPECT_TRUE(imagesEqual(hit.Get(), sync.Get()));
}
// ── C3-3缓存有界 LRU——请求/预取 > 容量个不同 target → size ≤ N ────────────
TEST(AsyncRegionBuilder, CacheIsBoundedLru) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_lru").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
const std::size_t cap = 3;
AsyncRegionBuilder b(dir, cap);
// 提交 6 个不同 target> 容量),逐个等就绪。
std::vector<RegionTarget> ts = {
makeTarget(0, 0, 1, 0, 1, 0, 1), makeTarget(0, 1, 2, 0, 1, 0, 1),
makeTarget(0, 2, 3, 0, 1, 0, 1), makeTarget(1, 0, 1, 0, 1, 0, 1),
makeTarget(1, 1, 2, 0, 1, 0, 1), makeTarget(2, 0, 1, 0, 1, 0, 1),
};
for (const auto& t : ts) {
b.requestTarget(t);
ASSERT_NE(waitGetReady(b, t).Get(), nullptr) << "target 未就绪";
}
// 缓存有界size ≤ 容量。
EXPECT_LE(b.cacheSize(), cap);
// 最久未用的第一个应被淘汰getReady 即刻 nullptr不重建——非阻塞
int lv = -1;
EXPECT_EQ(b.getReady(ts.front(), lv).Get(), nullptr)
<< "最久未用项应已被 LRU 淘汰";
// 最近的几个仍命中。
EXPECT_NE(b.getReady(ts.back(), lv).Get(), nullptr);
}
// ── C3-3相同请求短路——同一 target 连请求多次不重复建(建一次后命中缓存)────
TEST(AsyncRegionBuilder, IdenticalRequestShortCircuits) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_short").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
AsyncRegionBuilder b(dir);
const RegionTarget t = makeTarget(1, 0, 2, 0, 1, 0, 1);
b.requestTarget(t);
ASSERT_NE(waitGetReady(b, t).Get(), nullptr);
// 已就绪后再连发同一 target短路不应再有 pending/在建),缓存仍 1 条。
for (int i = 0; i < 10; ++i) b.requestTarget(t);
// 给 worker 一点时间(若误重建会出现 pending/building
sleepMs(50);
EXPECT_FALSE(b.hasPending()) << "同一已就绪 target 不应触发重建";
EXPECT_EQ(b.cacheSize(), 1u) << "同一 target 不应在缓存里重复占位";
// 短路时主目标仍可经 takeLatest 取到(兼容 C3-1/C3-2
int lv = -1;
EXPECT_NE(b.getReady(t, lv).Get(), nullptr);
}
// ── C3-3预取不饿死主目标——大量 prefetch + 一个主 requestTarget → 主先就绪 ──
TEST(AsyncRegionBuilder, PrefetchDoesNotStarveMainTarget) {
const auto dir =
(std::filesystem::temp_directory_path() / "gpr_arb_starve").string();
makePyramidStore(dir, 200, 80, 60, 64, 3);
AsyncRegionBuilder b(dir, /*cap*/ 6);
// 先灌大量预取(不同 target再设主目标。
std::vector<RegionTarget> many;
for (int i = 0; i < 12; ++i)
many.push_back(makeTarget(0, i % 3, (i % 3) + 1, 0, 1, 0, 1, 1.0 + i));
b.prefetch(many);
const RegionTarget main = makeTarget(2, 0, 1, 0, 1, 0, 1);
b.requestTarget(main);
// 主目标应优先就绪(轮询 getReady(main) 命中)。
ASSERT_NE(waitGetReady(b, main, 800, 2).Get(), nullptr)
<< "预取饿死了主目标";
}