勉强完成延迟很高
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <web_server/src/detail/Gallery_Frame_Atlas.hpp>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::web::detail {
|
||||
namespace {
|
||||
std::shared_ptr<const Plot_Pixel_Frame> solid_frame(
|
||||
std::uint64_t sequence, std::uint64_t correlation_id,
|
||||
std::uint32_t width, std::uint32_t height,
|
||||
std::byte red, std::byte green, std::byte blue,
|
||||
std::uint64_t rendered_sequence = 0,
|
||||
std::uint64_t rendered_correlation_id = 0) {
|
||||
auto pixels = std::make_shared<std::vector<std::byte>>(
|
||||
static_cast<std::size_t>(width) * height * 4U);
|
||||
for (std::size_t offset = 0; offset < pixels->size(); offset += 4U) {
|
||||
(*pixels)[offset] = red;
|
||||
(*pixels)[offset + 1U] = green;
|
||||
(*pixels)[offset + 2U] = blue;
|
||||
(*pixels)[offset + 3U] = std::byte{255};
|
||||
}
|
||||
return std::make_shared<const Plot_Pixel_Frame>(Plot_Pixel_Frame{
|
||||
std::move(pixels), {}, sequence, correlation_id,
|
||||
rendered_sequence == 0 ? sequence : rendered_sequence,
|
||||
rendered_correlation_id == 0 ? correlation_id
|
||||
: rendered_correlation_id,
|
||||
width, height});
|
||||
}
|
||||
|
||||
std::size_t pixel_offset(const Gallery_Atlas_Description& description,
|
||||
std::uint32_t x, std::uint32_t y) {
|
||||
return (static_cast<std::size_t>(y) * description.width + x) * 4U;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Gallery_Frame_Atlas, Layout_And_Changed_Tiles_Have_One_Authority) {
|
||||
Gallery_Frame_Atlas atlas(2, 2, 2, {"alpha", "beta", "gamma"});
|
||||
const auto description = atlas.describe();
|
||||
ASSERT_EQ(description.columns, 2U);
|
||||
ASSERT_EQ(description.rows, 2U);
|
||||
ASSERT_EQ(description.width, 4U);
|
||||
ASSERT_EQ(description.height, 4U);
|
||||
ASSERT_EQ(description.sources.size(), 3U);
|
||||
EXPECT_EQ(description.sources[2].column, 0U);
|
||||
EXPECT_EQ(description.sources[2].row, 1U);
|
||||
|
||||
EXPECT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(1, 11, 2, 2, std::byte{10},
|
||||
std::byte{20}, std::byte{30})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
EXPECT_EQ(atlas.accept_frame(
|
||||
2, solid_frame(7, 17, 2, 2, std::byte{70},
|
||||
std::byte{80}, std::byte{90})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
|
||||
const auto first = atlas.compose();
|
||||
ASSERT_EQ(first.rgba.size(), 4U * 4U * 4U);
|
||||
EXPECT_EQ(first.fresh_tile_count, 2U);
|
||||
EXPECT_EQ(first.missing_tile_count, 1U);
|
||||
EXPECT_EQ(first.sources[0].rendered_correlation_id, 11U);
|
||||
EXPECT_EQ(first.sources[2].completion_count, 1U);
|
||||
EXPECT_EQ(first.sources[2].rendered_frame_count, 1U);
|
||||
const auto alpha = pixel_offset(description, 0, 0);
|
||||
const auto gamma = pixel_offset(description, 0, 2);
|
||||
EXPECT_EQ(first.rgba[alpha], std::byte{10});
|
||||
EXPECT_EQ(first.rgba[alpha + 3U], std::byte{255});
|
||||
EXPECT_EQ(first.rgba[gamma], std::byte{70});
|
||||
EXPECT_EQ(first.rgba[gamma + 2U], std::byte{90});
|
||||
|
||||
const auto unchanged = atlas.compose();
|
||||
EXPECT_EQ(unchanged.fresh_tile_count, 0U);
|
||||
EXPECT_EQ(unchanged.missing_tile_count, 1U);
|
||||
}
|
||||
|
||||
TEST(Gallery_Frame_Atlas, Invalid_And_Stale_Frames_Do_Not_Replace_Latest) {
|
||||
Gallery_Frame_Atlas atlas(2, 2, 1, {"only"});
|
||||
EXPECT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(4, 14, 2, 2, std::byte{4},
|
||||
std::byte{5}, std::byte{6})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
EXPECT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(3, 13, 2, 2, std::byte{30},
|
||||
std::byte{31}, std::byte{32})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::stale_frame);
|
||||
EXPECT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(5, 15, 4, 2, std::byte{50},
|
||||
std::byte{51}, std::byte{52})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::invalid_frame);
|
||||
const auto composition = atlas.compose();
|
||||
EXPECT_EQ(composition.rejected_frame_count, 2U);
|
||||
EXPECT_EQ(composition.sources[0].completion_sequence, 4U);
|
||||
EXPECT_EQ(composition.rgba[0], std::byte{4});
|
||||
EXPECT_EQ(atlas.compose().rejected_frame_count, 0U);
|
||||
}
|
||||
|
||||
TEST(Gallery_Frame_Atlas, Logical_History_Callback_Does_Not_Fake_A_New_Image) {
|
||||
Gallery_Frame_Atlas atlas(2, 2, 1, {"only"});
|
||||
ASSERT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(4, 14, 2, 2, std::byte{4},
|
||||
std::byte{5}, std::byte{6})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
ASSERT_EQ(atlas.compose().fresh_tile_count, 1U);
|
||||
ASSERT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(5, 15, 2, 2, std::byte{50},
|
||||
std::byte{51}, std::byte{52}, 4, 14)),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
const auto composition = atlas.compose();
|
||||
EXPECT_EQ(composition.fresh_tile_count, 0U);
|
||||
EXPECT_EQ(composition.sources[0].completion_count, 2U);
|
||||
EXPECT_EQ(composition.sources[0].rendered_frame_count, 1U);
|
||||
EXPECT_EQ(composition.sources[0].completion_sequence, 5U);
|
||||
EXPECT_EQ(composition.sources[0].rendered_sequence, 4U);
|
||||
EXPECT_EQ(composition.rgba[0], std::byte{4});
|
||||
}
|
||||
|
||||
TEST(Gallery_Frame_Atlas, Diagnostics_Only_Completion_Preserves_Progress_And_Image) {
|
||||
Gallery_Frame_Atlas atlas(2, 2, 1, {"only"});
|
||||
ASSERT_EQ(atlas.accept_frame(
|
||||
0, solid_frame(1, 11, 2, 2, std::byte{7},
|
||||
std::byte{8}, std::byte{9})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
ASSERT_EQ(atlas.compose().fresh_tile_count, 1U);
|
||||
|
||||
auto diagnostics = std::make_shared<const Plot_Pixel_Frame>(
|
||||
Plot_Pixel_Frame{{}, {}, 2, 12, 2, 12, 2, 2});
|
||||
ASSERT_EQ(atlas.accept_frame(0, std::move(diagnostics)),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
const auto composition = atlas.compose();
|
||||
EXPECT_EQ(composition.fresh_tile_count, 0U);
|
||||
EXPECT_EQ(composition.missing_tile_count, 0U);
|
||||
EXPECT_EQ(composition.sources[0].completion_sequence, 2U);
|
||||
EXPECT_EQ(composition.sources[0].rendered_sequence, 2U);
|
||||
EXPECT_EQ(composition.sources[0].completion_count, 2U);
|
||||
EXPECT_EQ(composition.sources[0].rendered_frame_count, 2U);
|
||||
EXPECT_EQ(composition.rgba[0], std::byte{7});
|
||||
}
|
||||
|
||||
TEST(Gallery_Frame_Atlas, Plot_Producers_Accept_In_Parallel_While_Encoding_Composes) {
|
||||
constexpr std::size_t source_count{8};
|
||||
constexpr std::uint64_t frame_count{500};
|
||||
std::vector<std::string> ids;
|
||||
ids.reserve(source_count);
|
||||
for (std::size_t slot = 0; slot < source_count; ++slot)
|
||||
ids.push_back("source-" + std::to_string(slot));
|
||||
Gallery_Frame_Atlas atlas(2, 2, 4, std::move(ids));
|
||||
|
||||
std::atomic_size_t completed_producers{};
|
||||
std::vector<std::jthread> producers;
|
||||
producers.reserve(source_count);
|
||||
for (std::size_t slot = 0; slot < source_count; ++slot) {
|
||||
producers.emplace_back([&, slot] {
|
||||
for (std::uint64_t sequence = 1; sequence <= frame_count;
|
||||
++sequence) {
|
||||
const auto color = std::byte{
|
||||
static_cast<unsigned char>((slot + sequence) % 251U)};
|
||||
EXPECT_EQ(atlas.accept_frame(
|
||||
slot, solid_frame(
|
||||
sequence, sequence, 2, 2, color,
|
||||
std::byte{static_cast<unsigned char>(slot)},
|
||||
std::byte{0})),
|
||||
Gallery_Frame_Atlas::Accept_Frame_Result::accepted);
|
||||
}
|
||||
completed_producers.fetch_add(1, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
while (completed_producers.load(std::memory_order_acquire) < source_count)
|
||||
static_cast<void>(atlas.compose());
|
||||
for (auto& producer : producers) producer.join();
|
||||
|
||||
const auto final = atlas.compose();
|
||||
ASSERT_EQ(final.sources.size(), source_count);
|
||||
EXPECT_EQ(final.missing_tile_count, 0U);
|
||||
EXPECT_EQ(final.rejected_frame_count, 0U);
|
||||
for (const auto& source : final.sources) {
|
||||
EXPECT_EQ(source.completion_sequence, frame_count);
|
||||
EXPECT_EQ(source.rendered_sequence, frame_count);
|
||||
EXPECT_EQ(source.completion_count, frame_count);
|
||||
EXPECT_EQ(source.rendered_frame_count, frame_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <web_server/src/detail/Gallery_Frame_Clock.hpp>
|
||||
#include <asio/thread_pool.hpp>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::web::detail {
|
||||
TEST(Gallery_Frame_Clock, Produces_One_Monotonic_Absolute_Timeline) {
|
||||
asio::thread_pool pool(1);
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
std::vector<Plot_Render_Tick> ticks;
|
||||
std::exception_ptr failure;
|
||||
{
|
||||
Gallery_Frame_Clock clock(
|
||||
pool.get_executor(), 100.0,
|
||||
[&](Plot_Render_Tick tick) {
|
||||
std::lock_guard lock(mutex);
|
||||
ticks.push_back(tick);
|
||||
condition.notify_all();
|
||||
},
|
||||
[&](std::exception_ptr value) {
|
||||
std::lock_guard lock(mutex);
|
||||
failure = std::move(value);
|
||||
condition.notify_all();
|
||||
});
|
||||
clock.start();
|
||||
clock.start();
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
ASSERT_TRUE(condition.wait_for(lock, std::chrono::seconds(2), [&] {
|
||||
return ticks.size() >= 8 || failure;
|
||||
}));
|
||||
ASSERT_FALSE(failure);
|
||||
ASSERT_GE(ticks.size(), 8U);
|
||||
for (std::size_t index = 1; index < ticks.size(); ++index) {
|
||||
EXPECT_GT(ticks[index].sequence, ticks[index - 1].sequence);
|
||||
EXPECT_GT(ticks[index].time_milliseconds,
|
||||
ticks[index - 1].time_milliseconds);
|
||||
}
|
||||
EXPECT_NEAR(ticks.front().time_milliseconds,
|
||||
static_cast<double>(ticks.front().sequence) * 10.0,
|
||||
0.001);
|
||||
}
|
||||
clock.stop();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
std::size_t stopped_count{};
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
stopped_count = ticks.size();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
EXPECT_EQ(ticks.size(), stopped_count);
|
||||
}
|
||||
}
|
||||
pool.stop();
|
||||
pool.join();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <web_server/src/H264_Encoder.hpp>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::web {
|
||||
namespace {
|
||||
std::vector<std::byte> test_pattern(std::uint32_t width,
|
||||
std::uint32_t height) {
|
||||
std::vector<std::byte> result(
|
||||
static_cast<std::size_t>(width) * height * 4U);
|
||||
for (std::uint32_t y = 0; y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
const auto offset =
|
||||
(static_cast<std::size_t>(y) * width + x) * 4U;
|
||||
result[offset] = std::byte{static_cast<std::uint8_t>(x)};
|
||||
result[offset + 1U] = std::byte{static_cast<std::uint8_t>(y)};
|
||||
result[offset + 2U] =
|
||||
std::byte{static_cast<std::uint8_t>(x ^ y)};
|
||||
result[offset + 3U] = std::byte{255};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(H264_Encoder, Rejects_Invalid_Input_Before_FFmpeg) {
|
||||
H264_Encoder encoder(100.0);
|
||||
const auto pixels = test_pattern(320, 192);
|
||||
EXPECT_THROW(static_cast<void>(encoder.encode(
|
||||
pixels, 319, 192, Video_Pixel_Layout::rgba,
|
||||
1, std::chrono::microseconds{10'000})),
|
||||
std::invalid_argument);
|
||||
EXPECT_THROW(static_cast<void>(encoder.encode(
|
||||
std::span<const std::byte>{pixels}.first(pixels.size() - 1U),
|
||||
320, 192, Video_Pixel_Layout::rgba, 1,
|
||||
std::chrono::microseconds{10'000})),
|
||||
std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(H264_Encoder, Hardware_Backend_Produces_Annex_B_At_One_Hundred_Fps) {
|
||||
constexpr std::uint32_t width{1'920};
|
||||
constexpr std::uint32_t height{768};
|
||||
H264_Encoder encoder(100.0);
|
||||
auto pixels = test_pattern(width, height);
|
||||
std::optional<Encoded_Video_Frame> first;
|
||||
for (std::uint64_t sequence = 1; sequence <= 4 && !first; ++sequence) {
|
||||
first = encoder.encode(
|
||||
pixels, width, height, Video_Pixel_Layout::rgba, sequence,
|
||||
std::chrono::microseconds{static_cast<std::int64_t>(sequence * 10'000)});
|
||||
}
|
||||
ASSERT_TRUE(first);
|
||||
ASSERT_FALSE(first->annex_b.empty());
|
||||
EXPECT_TRUE(first->key_frame);
|
||||
ASSERT_GE(first->annex_b.size(), 4U);
|
||||
EXPECT_EQ(first->annex_b[0], std::byte{0});
|
||||
EXPECT_EQ(first->annex_b[1], std::byte{0});
|
||||
EXPECT_TRUE((first->annex_b[2] == std::byte{1}) ||
|
||||
(first->annex_b[2] == std::byte{0} &&
|
||||
first->annex_b[3] == std::byte{1}));
|
||||
EXPECT_NE(video_encoder_backend_name(first->backend), "unknown");
|
||||
EXPECT_EQ(h264_profile_level_id(), "640033");
|
||||
}
|
||||
|
||||
TEST(H264_Encoder, Hardware_Pipeline_Drains_A_Continuous_Frame_Sequence) {
|
||||
constexpr std::uint32_t width{1'920};
|
||||
constexpr std::uint32_t height{768};
|
||||
H264_Encoder encoder(100.0);
|
||||
auto pixels = test_pattern(width, height);
|
||||
std::uint64_t produced{};
|
||||
std::uint64_t latest_sequence{};
|
||||
std::optional<Video_Encoder_Backend> backend;
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
for (std::uint64_t sequence = 1; sequence <= 120; ++sequence) {
|
||||
pixels[0] = std::byte{static_cast<std::uint8_t>(sequence)};
|
||||
const auto frame = encoder.encode(
|
||||
pixels, width, height, Video_Pixel_Layout::rgba, sequence,
|
||||
std::chrono::microseconds{
|
||||
static_cast<std::int64_t>(sequence * 10'000)});
|
||||
if (!frame) continue;
|
||||
++produced;
|
||||
EXPECT_GT(frame->sequence, latest_sequence);
|
||||
latest_sequence = frame->sequence;
|
||||
EXPECT_FALSE(frame->annex_b.empty());
|
||||
if (!backend) backend = frame->backend;
|
||||
EXPECT_EQ(frame->backend, *backend);
|
||||
}
|
||||
const auto elapsed = std::chrono::steady_clock::now() - started;
|
||||
EXPECT_GE(produced, 115U);
|
||||
EXPECT_GE(latest_sequence, 115U);
|
||||
EXPECT_LE(elapsed, std::chrono::milliseconds{1'200});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user