blob: 7e9df59fb390837a3283c2f7463637ee2edf3700 [file]
// Hidden benchmark for capture-phase listeners. Not part of the suite: build
// the tests in release and run ./out/release/unit_tests "[.capture_bench]".
// It compiles without the feature too, so copying it into an older tree
// gives a baseline. Three regimes per scene:
// A no machine anywhere has a capture listener
// B another file's machine has one (the pass must stay off in this tree)
// C this tree has one (its first pointer listener flipped to capture)
#include "rive/animation/nested_state_machine.hpp"
#include "rive/animation/state_machine.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/animation/state_machine_listener.hpp"
#include "rive/artboard_component_list.hpp"
#include "rive/nested_artboard.hpp"
#include "rive_file_reader.hpp"
#include "rive_testing.hpp"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <functional>
#include <string>
#include <vector>
#if __has_include("rive/listener_phase.hpp")
#include "rive/listener_phase.hpp"
#define HAS_CAPTURE 1
#else
#define HAS_CAPTURE 0
#endif
using namespace rive;
namespace
{
using Clock = std::chrono::steady_clock;
void forEachMachine(
StateMachineInstance* smi,
const std::function<void(StateMachineInstance*)>& visit)
{
visit(smi);
for (auto nested : smi->artboard()->nestedArtboards())
{
for (auto animation : nested->nestedAnimations())
{
if (animation->is<NestedStateMachine>())
{
if (auto child = animation->as<NestedStateMachine>()
->stateMachineInstance())
{
forEachMachine(child, visit);
}
}
}
}
for (auto list : smi->artboard()->artboardComponentLists())
{
for (int i : list->orderedListIndices())
{
if (auto child = list->stateMachineInstance(i))
{
forEachMachine(child, visit);
}
}
}
}
rcp<File> load(const std::string& path)
{
auto bytes = ReadFile(path.c_str());
ImportResult result;
auto file = File::import(bytes, &gNoOpFactory, &result);
if (result != ImportResult::success || file == nullptr ||
file->artboard() == nullptr)
{
return nullptr;
}
return file;
}
struct BenchInstance
{
std::unique_ptr<ArtboardInstance> artboard;
std::unique_ptr<StateMachineInstance> machine;
explicit BenchInstance(File* file, const char* artboardName = nullptr)
{
artboard = artboardName != nullptr ? file->artboardNamed(artboardName)
: file->artboardDefault();
if (artboard == nullptr || artboard->stateMachineCount() == 0)
{
artboard = nullptr;
return;
}
machine = artboard->stateMachineAt(0);
// The first update builds the draw order, the next advance sorts the
// hit components by it.
machine->advanceAndApply(0.0f);
machine->advanceAndApply(0.0f);
}
};
#if HAS_CAPTURE
// Flips the first pointer listener of the file's artboard's first machine
// (or the named one) to the capture phase. Returns its name.
std::string captureFirstPointerListener(
File* file,
const char* artboardName,
const char* listenerName = nullptr)
{
Artboard* artboard = artboardName != nullptr ? file->artboard(artboardName)
: file->artboard();
if (artboard == nullptr || artboard->stateMachineCount() == 0)
{
return "";
}
auto machine = artboard->stateMachine(0);
for (size_t i = 0; i < machine->listenerCount(); i++)
{
auto listener = machine->listener(i);
if (!listener->hasPointerListeners())
{
continue;
}
if (listenerName != nullptr && listener->name() != listenerName)
{
continue;
}
const_cast<StateMachineListener*>(listener)->phaseValue(
(uint32_t)ListenerPhase::capture);
return listener->name().empty() ? "(unnamed)" : listener->name();
}
return "";
}
#endif
// Pointer moves over a grid across the artboard, in blocks with an untimed
// advance between them so reported events and hover work don't pile up.
// Returns the best of `runs` runs, in nanoseconds per event.
double timeMoves(StateMachineInstance* machine, int runs = 7)
{
const float width = machine->artboard()->width();
const float height = machine->artboard()->height();
const int grid = 24;
std::vector<Vec2D> points;
for (int y = 0; y < grid; y++)
{
for (int x = 0; x < grid; x++)
{
float px = (x + 0.5f) * width / grid;
float py = (y + 0.5f) * height / grid;
// Serpentine, so consecutive moves stay close like a real drag.
points.push_back(Vec2D(y % 2 == 0 ? px : width - px, py));
}
}
double best = 1e30;
for (int run = 0; run < runs; run++)
{
Clock::duration total{};
size_t events = 0;
for (int pass = 0; pass < 8; pass++)
{
for (size_t start = 0; start < points.size(); start += 64)
{
auto t0 = Clock::now();
size_t end = std::min(points.size(), start + 64);
for (size_t i = start; i < end; i++)
{
machine->pointerMove(points[i]);
}
total += Clock::now() - t0;
events += end - start;
machine->advanceAndApply(0.0f);
}
}
double ns = std::chrono::duration<double, std::nano>(total).count() /
events;
best = std::min(best, ns);
}
return best;
}
// A press and release at the artboard's center, the gesture a claim hangs on.
double timeClicks(StateMachineInstance* machine, int runs = 7)
{
Vec2D center(
machine->artboard()->width() * 0.5f,
machine->artboard()->height() * 0.5f);
double best = 1e30;
for (int run = 0; run < runs; run++)
{
Clock::duration total{};
const int clicks = 512;
for (int i = 0; i < clicks; i++)
{
auto t0 = Clock::now();
machine->pointerDown(center);
machine->pointerUp(center);
total += Clock::now() - t0;
if (i % 32 == 31)
{
machine->advanceAndApply(0.0f);
}
}
double ns = std::chrono::duration<double, std::nano>(total).count() /
(clicks * 2);
best = std::min(best, ns);
}
return best;
}
// Instancing the artboard (which builds every nested machine) plus the root
// machine, then tearing both down.
double timeInit(File* file, const char* artboardName, int runs = 5)
{
double best = 1e30;
for (int run = 0; run < runs; run++)
{
const int count = 64;
auto t0 = Clock::now();
for (int i = 0; i < count; i++)
{
auto artboard = artboardName != nullptr
? file->artboardNamed(artboardName)
: file->artboardDefault();
auto machine = artboard->stateMachineAt(0);
}
double us = std::chrono::duration<double, std::micro>(Clock::now() - t0)
.count() /
count;
best = std::min(best, us);
}
return best;
}
// One frame of advanceAndApply, without input.
double timeAdvance(StateMachineInstance* machine, int runs = 7)
{
double best = 1e30;
for (int run = 0; run < runs; run++)
{
const int frames = 256;
auto t0 = Clock::now();
for (int i = 0; i < frames; i++)
{
machine->advanceAndApply(1.0f / 60.0f);
}
double us = std::chrono::duration<double, std::micro>(Clock::now() - t0)
.count() /
frames;
best = std::min(best, us);
}
return best;
}
struct BenchScene
{
const char* path;
const char* artboard;
// For the capture-in-tree run on the feature's own fixture, a listener
// that only watches moves, so nothing gets claimed mid-run.
const char* captureListener;
};
const BenchScene kScenes[] = {
{"assets/capture_listeners.riv", "Host", "Slot B Move"},
{"assets/layoutstest_8-planets-grid.riv", nullptr, nullptr},
{"assets/inventory_demo_test_v2.riv", nullptr, nullptr},
{"assets/magic_alley_db_reduced_export.riv", nullptr, nullptr},
};
} // namespace
TEST_CASE("capture listener benchmark", "[.capture_bench]")
{
printf("feature compiled in: %s\n", HAS_CAPTURE ? "yes" : "no");
for (const auto& benchScene : kScenes)
{
auto file = load(benchScene.path);
if (file == nullptr)
{
printf("%s: could not load\n", benchScene.path);
continue;
}
size_t machines = 0, hits = 0;
{
BenchInstance scene(file.get(), benchScene.artboard);
REQUIRE(scene.machine != nullptr);
forEachMachine(scene.machine.get(), [&](StateMachineInstance* smi) {
machines++;
hits += smi->hitComponentsCount();
});
}
printf(
"\n%s (%zu machines, %zu hit components)\n",
benchScene.path,
machines,
hits);
printf(" init %9.2f us\n", timeInit(file.get(), benchScene.artboard));
{
BenchInstance scene(file.get(), benchScene.artboard);
printf(" advance %9.2f us/frame\n", timeAdvance(scene.machine.get()));
printf(
" A no capture anywhere move %8.1f ns click %8.1f ns\n",
timeMoves(scene.machine.get()),
timeClicks(scene.machine.get()));
}
#if HAS_CAPTURE
{
// Another file's machine with a capture listener stays alive, which
// turns the capture phase on process-wide.
auto other = load("assets/capture_listeners.riv");
REQUIRE(other != nullptr);
captureFirstPointerListener(other.get(), "Host", "Slot B Move");
BenchInstance elsewhere(other.get(), "Host");
REQUIRE(StateMachineInstance::liveCaptureMachines() == 1);
BenchInstance scene(file.get(), benchScene.artboard);
printf(
" B capture elsewhere move %8.1f ns click %8.1f ns\n",
timeMoves(scene.machine.get()),
timeClicks(scene.machine.get()));
}
{
auto captured = load(benchScene.path);
auto name = captureFirstPointerListener(
captured.get(),
benchScene.artboard,
benchScene.captureListener);
if (name.empty())
{
printf(" C capture in tree (no pointer listener)\n");
}
else
{
printf(
" init with capture %9.2f us\n",
timeInit(captured.get(), benchScene.artboard));
BenchInstance scene(captured.get(), benchScene.artboard);
printf(
" C capture in tree move %8.1f ns click %8.1f ns"
" (%s)\n",
timeMoves(scene.machine.get()),
timeClicks(scene.machine.get()),
name.c_str());
}
}
#endif
}
}