mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-08-27 21:19:46 +00:00
Align UI storage event runtime contracts
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace sys
|
||||
{
|
||||
namespace runtime
|
||||
{
|
||||
|
||||
enum class NoticeCategory : uint8_t
|
||||
{
|
||||
General,
|
||||
ChatDelivery,
|
||||
Storage,
|
||||
Tracker,
|
||||
Protocol,
|
||||
};
|
||||
|
||||
enum class NoticeSeverity : uint8_t
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
};
|
||||
|
||||
enum class FeedbackEventKind : uint8_t
|
||||
{
|
||||
Posted,
|
||||
Presented,
|
||||
Dropped,
|
||||
Deduped,
|
||||
};
|
||||
|
||||
struct NoticeIntent
|
||||
{
|
||||
static constexpr std::size_t kMaxMessageBytes = 192;
|
||||
|
||||
uint32_t notice_id = 0;
|
||||
NoticeCategory category = NoticeCategory::General;
|
||||
NoticeSeverity severity = NoticeSeverity::Info;
|
||||
char message[kMaxMessageBytes]{};
|
||||
uint32_t duration_ms = 0;
|
||||
uint32_t dedupe_key = 0;
|
||||
uint32_t created_at_ms = 0;
|
||||
};
|
||||
|
||||
inline void setNoticeMessage(NoticeIntent& intent, const char* message)
|
||||
{
|
||||
std::strncpy(intent.message, message ? message : "", NoticeIntent::kMaxMessageBytes - 1);
|
||||
intent.message[NoticeIntent::kMaxMessageBytes - 1] = '\0';
|
||||
}
|
||||
|
||||
struct FeedbackEvent
|
||||
{
|
||||
FeedbackEventKind kind = FeedbackEventKind::Dropped;
|
||||
uint32_t notice_id = 0;
|
||||
uint32_t timestamp_ms = 0;
|
||||
int32_t error = 0;
|
||||
};
|
||||
|
||||
class FeedbackPolicy
|
||||
{
|
||||
public:
|
||||
virtual ~FeedbackPolicy() = default;
|
||||
|
||||
virtual bool shouldShow(const NoticeIntent& intent) const = 0;
|
||||
virtual bool dedupe(const NoticeIntent& previous, const NoticeIntent& next) const = 0;
|
||||
virtual uint32_t duration(const NoticeIntent& intent) const = 0;
|
||||
};
|
||||
|
||||
class DefaultFeedbackPolicy : public FeedbackPolicy
|
||||
{
|
||||
public:
|
||||
bool shouldShow(const NoticeIntent& intent) const override
|
||||
{
|
||||
return intent.message[0] != '\0';
|
||||
}
|
||||
|
||||
bool dedupe(const NoticeIntent& previous, const NoticeIntent& next) const override
|
||||
{
|
||||
if (next.dedupe_key != 0 && previous.dedupe_key == next.dedupe_key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return previous.category == next.category && previous.severity == next.severity &&
|
||||
std::strcmp(previous.message, next.message) == 0;
|
||||
}
|
||||
|
||||
uint32_t duration(const NoticeIntent& intent) const override
|
||||
{
|
||||
if (intent.duration_ms != 0)
|
||||
{
|
||||
return intent.duration_ms;
|
||||
}
|
||||
return intent.severity == NoticeSeverity::Error ? 2200 : 1400;
|
||||
}
|
||||
};
|
||||
|
||||
class IFeedbackPresenter
|
||||
{
|
||||
public:
|
||||
virtual ~IFeedbackPresenter() = default;
|
||||
|
||||
virtual bool present(const NoticeIntent& intent) = 0;
|
||||
};
|
||||
|
||||
class IFeedbackEventSink
|
||||
{
|
||||
public:
|
||||
virtual ~IFeedbackEventSink() = default;
|
||||
|
||||
virtual bool publish(const FeedbackEvent& event) = 0;
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
class FeedbackQueue
|
||||
{
|
||||
public:
|
||||
bool enqueue(const NoticeIntent& intent)
|
||||
{
|
||||
if (count_ >= N)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
notices_[(head_ + count_) % N] = intent;
|
||||
++count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool popReady(uint32_t now_ms, NoticeIntent& out)
|
||||
{
|
||||
(void)now_ms;
|
||||
if (count_ == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out = notices_[head_];
|
||||
head_ = (head_ + 1) % N;
|
||||
--count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool dedupe(const NoticeIntent& intent, const FeedbackPolicy& policy)
|
||||
{
|
||||
for (std::size_t i = 0; i < count_; ++i)
|
||||
{
|
||||
NoticeIntent& queued = notices_[(head_ + i) % N];
|
||||
if (policy.dedupe(queued, intent))
|
||||
{
|
||||
queued = intent;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t size() const
|
||||
{
|
||||
return count_;
|
||||
}
|
||||
|
||||
private:
|
||||
NoticeIntent notices_[N]{};
|
||||
std::size_t head_ = 0;
|
||||
std::size_t count_ = 0;
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
class FeedbackRuntime
|
||||
{
|
||||
public:
|
||||
FeedbackRuntime(FeedbackQueue<N>& queue,
|
||||
FeedbackPolicy& policy,
|
||||
IFeedbackPresenter& presenter,
|
||||
IFeedbackEventSink& events)
|
||||
: queue_(queue), policy_(policy), presenter_(presenter), events_(events)
|
||||
{
|
||||
}
|
||||
|
||||
bool post(NoticeIntent intent)
|
||||
{
|
||||
if (!policy_.shouldShow(intent))
|
||||
{
|
||||
publish(FeedbackEventKind::Dropped, intent.notice_id, intent.created_at_ms, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (intent.notice_id == 0)
|
||||
{
|
||||
intent.notice_id = next_notice_id_++;
|
||||
}
|
||||
intent.duration_ms = policy_.duration(intent);
|
||||
|
||||
if (queue_.dedupe(intent, policy_))
|
||||
{
|
||||
publish(FeedbackEventKind::Deduped, intent.notice_id, intent.created_at_ms, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool queued = queue_.enqueue(intent);
|
||||
publish(queued ? FeedbackEventKind::Posted : FeedbackEventKind::Dropped,
|
||||
intent.notice_id,
|
||||
intent.created_at_ms,
|
||||
queued ? 0 : -2);
|
||||
return queued;
|
||||
}
|
||||
|
||||
void handle(const FeedbackEvent& event)
|
||||
{
|
||||
last_event_ = event;
|
||||
}
|
||||
|
||||
std::size_t drainToUi(uint32_t now_ms)
|
||||
{
|
||||
std::size_t presented = 0;
|
||||
NoticeIntent intent{};
|
||||
while (queue_.popReady(now_ms, intent))
|
||||
{
|
||||
if (presenter_.present(intent))
|
||||
{
|
||||
++presented;
|
||||
publish(FeedbackEventKind::Presented, intent.notice_id, now_ms, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
publish(FeedbackEventKind::Dropped, intent.notice_id, now_ms, -3);
|
||||
}
|
||||
}
|
||||
return presented;
|
||||
}
|
||||
|
||||
FeedbackEvent lastEvent() const
|
||||
{
|
||||
return last_event_;
|
||||
}
|
||||
|
||||
private:
|
||||
void publish(FeedbackEventKind kind, uint32_t notice_id, uint32_t timestamp_ms, int32_t error)
|
||||
{
|
||||
FeedbackEvent event{};
|
||||
event.kind = kind;
|
||||
event.notice_id = notice_id;
|
||||
event.timestamp_ms = timestamp_ms;
|
||||
event.error = error;
|
||||
last_event_ = event;
|
||||
(void)events_.publish(event);
|
||||
}
|
||||
|
||||
FeedbackQueue<N>& queue_;
|
||||
FeedbackPolicy& policy_;
|
||||
IFeedbackPresenter& presenter_;
|
||||
IFeedbackEventSink& events_;
|
||||
FeedbackEvent last_event_{};
|
||||
uint32_t next_notice_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace sys
|
||||
@@ -0,0 +1,406 @@
|
||||
#pragma once
|
||||
|
||||
#include "sys/runtime_async.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace sys
|
||||
{
|
||||
namespace runtime
|
||||
{
|
||||
|
||||
enum class PersistencePolicyMode : uint8_t
|
||||
{
|
||||
DebouncedSave,
|
||||
BatchSave,
|
||||
ImmediateCriticalSave,
|
||||
DropDuplicateSave,
|
||||
};
|
||||
|
||||
enum class PersistenceEventKind : uint8_t
|
||||
{
|
||||
SaveQueued,
|
||||
SaveStarted,
|
||||
SaveSucceeded,
|
||||
SaveFailed,
|
||||
SaveCoalesced,
|
||||
SaveDropped,
|
||||
};
|
||||
|
||||
struct StoreSnapshot
|
||||
{
|
||||
const uint8_t* data = nullptr;
|
||||
std::size_t len = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct StoreStorageResult
|
||||
{
|
||||
bool ok = false;
|
||||
std::size_t bytes = 0;
|
||||
int32_t error = 0;
|
||||
};
|
||||
|
||||
struct PersistenceCommand
|
||||
{
|
||||
uint32_t command_id = 0;
|
||||
const char* store_key = nullptr;
|
||||
PersistencePolicyMode policy = PersistencePolicyMode::DebouncedSave;
|
||||
uint32_t deadline_ms = 0;
|
||||
uint32_t created_at_ms = 0;
|
||||
};
|
||||
|
||||
struct PersistenceEvent
|
||||
{
|
||||
PersistenceEventKind kind = PersistenceEventKind::SaveFailed;
|
||||
const char* store_key = nullptr;
|
||||
uint32_t command_id = 0;
|
||||
uint32_t timestamp_ms = 0;
|
||||
int32_t error = 0;
|
||||
};
|
||||
|
||||
class PersistencePolicy
|
||||
{
|
||||
public:
|
||||
virtual ~PersistencePolicy() = default;
|
||||
|
||||
virtual PersistencePolicyMode modeFor(const char* store_key) const = 0;
|
||||
virtual uint32_t delayFor(PersistencePolicyMode policy) const = 0;
|
||||
virtual BusAccessPolicy busPolicyFor(PersistencePolicyMode policy) const = 0;
|
||||
};
|
||||
|
||||
class DefaultPersistencePolicy : public PersistencePolicy
|
||||
{
|
||||
public:
|
||||
PersistencePolicyMode modeFor(const char* store_key) const override
|
||||
{
|
||||
(void)store_key;
|
||||
return PersistencePolicyMode::DebouncedSave;
|
||||
}
|
||||
|
||||
uint32_t delayFor(PersistencePolicyMode policy) const override
|
||||
{
|
||||
switch (policy)
|
||||
{
|
||||
case PersistencePolicyMode::ImmediateCriticalSave:
|
||||
return 0;
|
||||
case PersistencePolicyMode::BatchSave:
|
||||
return 500;
|
||||
case PersistencePolicyMode::DropDuplicateSave:
|
||||
case PersistencePolicyMode::DebouncedSave:
|
||||
default:
|
||||
return 150;
|
||||
}
|
||||
}
|
||||
|
||||
BusAccessPolicy busPolicyFor(PersistencePolicyMode policy) const override
|
||||
{
|
||||
return policy == PersistencePolicyMode::ImmediateCriticalSave
|
||||
? BusAccessPolicy::DurableCommit
|
||||
: BusAccessPolicy::BackgroundWorkerBounded;
|
||||
}
|
||||
};
|
||||
|
||||
class IStoreSnapshotProvider
|
||||
{
|
||||
public:
|
||||
virtual ~IStoreSnapshotProvider() = default;
|
||||
|
||||
virtual StoreSnapshot snapshot(const char* store_key) = 0;
|
||||
};
|
||||
|
||||
class IStoreStorageAdapter
|
||||
{
|
||||
public:
|
||||
virtual ~IStoreStorageAdapter() = default;
|
||||
|
||||
virtual StoreStorageResult write(const char* store_key,
|
||||
const uint8_t* bytes,
|
||||
std::size_t len) = 0;
|
||||
virtual StoreStorageResult read(const char* store_key,
|
||||
uint8_t* bytes,
|
||||
std::size_t capacity,
|
||||
std::size_t& out_len) = 0;
|
||||
};
|
||||
|
||||
class IPersistenceEventSink
|
||||
{
|
||||
public:
|
||||
virtual ~IPersistenceEventSink() = default;
|
||||
|
||||
virtual bool publish(const PersistenceEvent& event) = 0;
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
class DirtyStoreRegistry
|
||||
{
|
||||
public:
|
||||
bool markDirty(const char* store_key,
|
||||
PersistencePolicyMode policy,
|
||||
uint32_t due_ms)
|
||||
{
|
||||
if (store_key == nullptr || store_key[0] == '\0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < count_; ++i)
|
||||
{
|
||||
if (sameKey(records_[i].store_key, store_key))
|
||||
{
|
||||
records_[i].policy = policy;
|
||||
records_[i].due_ms = due_ms;
|
||||
records_[i].dirty = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count_ >= N)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
records_[count_++] = DirtyStoreRecord{store_key, due_ms, policy, true};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool takeDue(uint32_t now_ms, PersistenceCommand& out)
|
||||
{
|
||||
for (std::size_t i = 0; i < count_; ++i)
|
||||
{
|
||||
DirtyStoreRecord& record = records_[i];
|
||||
if (!record.dirty || static_cast<int32_t>(record.due_ms - now_ms) > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
out.store_key = record.store_key;
|
||||
out.policy = record.policy;
|
||||
out.created_at_ms = now_ms;
|
||||
record.dirty = false;
|
||||
compact();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasPending(const char* store_key) const
|
||||
{
|
||||
for (std::size_t i = 0; i < count_; ++i)
|
||||
{
|
||||
if (records_[i].dirty && sameKey(records_[i].store_key, store_key))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t size() const
|
||||
{
|
||||
return count_;
|
||||
}
|
||||
|
||||
private:
|
||||
struct DirtyStoreRecord
|
||||
{
|
||||
const char* store_key = nullptr;
|
||||
uint32_t due_ms = 0;
|
||||
PersistencePolicyMode policy = PersistencePolicyMode::DebouncedSave;
|
||||
bool dirty = false;
|
||||
};
|
||||
|
||||
static bool sameKey(const char* lhs, const char* rhs)
|
||||
{
|
||||
return lhs != nullptr && rhs != nullptr && std::strcmp(lhs, rhs) == 0;
|
||||
}
|
||||
|
||||
void compact()
|
||||
{
|
||||
DirtyStoreRecord kept[N]{};
|
||||
std::size_t kept_count = 0;
|
||||
for (std::size_t i = 0; i < count_; ++i)
|
||||
{
|
||||
if (records_[i].dirty)
|
||||
{
|
||||
kept[kept_count++] = records_[i];
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < kept_count; ++i)
|
||||
{
|
||||
records_[i] = kept[i];
|
||||
}
|
||||
count_ = kept_count;
|
||||
}
|
||||
|
||||
DirtyStoreRecord records_[N]{};
|
||||
std::size_t count_ = 0;
|
||||
};
|
||||
|
||||
class PersistenceWorker
|
||||
{
|
||||
public:
|
||||
PersistenceWorker(IStoreSnapshotProvider& snapshots,
|
||||
IStoreStorageAdapter& storage,
|
||||
IBusArbiter& bus,
|
||||
IPersistenceEventSink& events,
|
||||
PersistencePolicy& policy)
|
||||
: snapshots_(snapshots), storage_(storage), bus_(bus), events_(events), policy_(policy)
|
||||
{
|
||||
}
|
||||
|
||||
bool submit(const PersistenceCommand& command)
|
||||
{
|
||||
if (pending_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pending_command_ = command;
|
||||
pending_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void tick(uint32_t now_ms)
|
||||
{
|
||||
if (!pending_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PersistenceEvent started{};
|
||||
started.kind = PersistenceEventKind::SaveStarted;
|
||||
started.store_key = pending_command_.store_key;
|
||||
started.command_id = pending_command_.command_id;
|
||||
started.timestamp_ms = now_ms;
|
||||
(void)events_.publish(started);
|
||||
|
||||
BusAcquireRequest request{};
|
||||
request.policy = policy_.busPolicyFor(pending_command_.policy);
|
||||
request.command_id = pending_command_.command_id;
|
||||
request.deadline_ms = pending_command_.deadline_ms;
|
||||
const BusAcquireResult acquire = bus_.acquire(request);
|
||||
if (acquire.status != BusAcquireStatus::Acquired)
|
||||
{
|
||||
publishResult(PersistenceEventKind::SaveFailed, now_ms, -11);
|
||||
pending_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
StoreSnapshot snapshot = snapshots_.snapshot(pending_command_.store_key);
|
||||
StoreStorageResult result{};
|
||||
if (snapshot.valid)
|
||||
{
|
||||
result = storage_.write(pending_command_.store_key, snapshot.data, snapshot.len);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.error = -12;
|
||||
}
|
||||
bus_.release(acquire.token);
|
||||
|
||||
publishResult(result.ok ? PersistenceEventKind::SaveSucceeded
|
||||
: PersistenceEventKind::SaveFailed,
|
||||
now_ms,
|
||||
result.ok ? 0 : result.error);
|
||||
pending_ = false;
|
||||
}
|
||||
|
||||
bool busy() const
|
||||
{
|
||||
return pending_;
|
||||
}
|
||||
|
||||
private:
|
||||
void publishResult(PersistenceEventKind kind, uint32_t now_ms, int32_t error)
|
||||
{
|
||||
PersistenceEvent event{};
|
||||
event.kind = kind;
|
||||
event.store_key = pending_command_.store_key;
|
||||
event.command_id = pending_command_.command_id;
|
||||
event.timestamp_ms = now_ms;
|
||||
event.error = error;
|
||||
(void)events_.publish(event);
|
||||
}
|
||||
|
||||
IStoreSnapshotProvider& snapshots_;
|
||||
IStoreStorageAdapter& storage_;
|
||||
IBusArbiter& bus_;
|
||||
IPersistenceEventSink& events_;
|
||||
PersistencePolicy& policy_;
|
||||
PersistenceCommand pending_command_{};
|
||||
bool pending_ = false;
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
class PersistenceRuntime
|
||||
{
|
||||
public:
|
||||
PersistenceRuntime(DirtyStoreRegistry<N>& registry,
|
||||
PersistenceWorker& worker,
|
||||
IPersistenceEventSink& events,
|
||||
PersistencePolicy& policy)
|
||||
: registry_(registry), worker_(worker), events_(events), policy_(policy)
|
||||
{
|
||||
}
|
||||
|
||||
bool markDirty(const char* store_key, uint32_t now_ms)
|
||||
{
|
||||
const PersistencePolicyMode mode = policy_.modeFor(store_key);
|
||||
const bool marked = registry_.markDirty(store_key, mode, now_ms + policy_.delayFor(mode));
|
||||
PersistenceEvent event{};
|
||||
event.kind = marked ? PersistenceEventKind::SaveQueued
|
||||
: PersistenceEventKind::SaveDropped;
|
||||
event.store_key = store_key;
|
||||
event.timestamp_ms = now_ms;
|
||||
(void)events_.publish(event);
|
||||
return marked;
|
||||
}
|
||||
|
||||
bool requestSave(const char* store_key,
|
||||
PersistencePolicyMode policy,
|
||||
uint32_t now_ms)
|
||||
{
|
||||
return registry_.markDirty(store_key, policy, now_ms + policy_.delayFor(policy));
|
||||
}
|
||||
|
||||
void tick(uint32_t now_ms)
|
||||
{
|
||||
if (!worker_.busy())
|
||||
{
|
||||
PersistenceCommand command{};
|
||||
if (registry_.takeDue(now_ms, command))
|
||||
{
|
||||
command.command_id = next_command_id_++;
|
||||
if (!worker_.submit(command))
|
||||
{
|
||||
(void)registry_.markDirty(command.store_key,
|
||||
command.policy,
|
||||
now_ms + policy_.delayFor(command.policy));
|
||||
}
|
||||
}
|
||||
}
|
||||
worker_.tick(now_ms);
|
||||
}
|
||||
|
||||
void handle(const PersistenceEvent& event)
|
||||
{
|
||||
last_event_ = event;
|
||||
}
|
||||
|
||||
PersistenceEvent lastEvent() const
|
||||
{
|
||||
return last_event_;
|
||||
}
|
||||
|
||||
private:
|
||||
DirtyStoreRegistry<N>& registry_;
|
||||
PersistenceWorker& worker_;
|
||||
IPersistenceEventSink& events_;
|
||||
PersistencePolicy& policy_;
|
||||
PersistenceEvent last_event_{};
|
||||
uint32_t next_command_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace sys
|
||||
@@ -270,6 +270,164 @@ class IBusArbiter
|
||||
virtual StorageHealthState health() const = 0;
|
||||
};
|
||||
|
||||
class IBusAdapter
|
||||
{
|
||||
public:
|
||||
virtual ~IBusAdapter() = default;
|
||||
|
||||
virtual bool tryAcquire(uint32_t timeout_ms) = 0;
|
||||
virtual void release() = 0;
|
||||
virtual uint32_t nowMs() const = 0;
|
||||
virtual uint32_t owner() const = 0;
|
||||
};
|
||||
|
||||
class BusPolicyStrategy
|
||||
{
|
||||
public:
|
||||
virtual ~BusPolicyStrategy() = default;
|
||||
|
||||
virtual BusAccessPolicy select(const RuntimeCommand& command) const = 0;
|
||||
virtual uint32_t timeoutFor(BusAccessPolicy policy) const = 0;
|
||||
};
|
||||
|
||||
class DefaultBusPolicyStrategy : public BusPolicyStrategy
|
||||
{
|
||||
public:
|
||||
BusAccessPolicy select(const RuntimeCommand& command) const override
|
||||
{
|
||||
if (command.priority == RuntimePriority::Realtime ||
|
||||
command.priority == RuntimePriority::Interactive)
|
||||
{
|
||||
return BusAccessPolicy::InteractiveWorkerBounded;
|
||||
}
|
||||
if (command.kind == RuntimeCommandKind::TrackStop ||
|
||||
command.kind == RuntimeCommandKind::TrackFlush)
|
||||
{
|
||||
return BusAccessPolicy::DurableCommit;
|
||||
}
|
||||
return BusAccessPolicy::BackgroundWorkerBounded;
|
||||
}
|
||||
|
||||
uint32_t timeoutFor(BusAccessPolicy policy) const override
|
||||
{
|
||||
switch (policy)
|
||||
{
|
||||
case BusAccessPolicy::UiNeverBlock:
|
||||
return 0;
|
||||
case BusAccessPolicy::InteractiveWorkerBounded:
|
||||
return 2;
|
||||
case BusAccessPolicy::BackgroundWorkerBounded:
|
||||
return 25;
|
||||
case BusAccessPolicy::DurableCommit:
|
||||
return 150;
|
||||
case BusAccessPolicy::RecoveryExclusive:
|
||||
return 500;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class StorageBusArbiter : public IBusArbiter
|
||||
{
|
||||
public:
|
||||
StorageBusArbiter(IBusAdapter& adapter, BusPolicyStrategy& policy)
|
||||
: adapter_(adapter), policy_(policy)
|
||||
{
|
||||
}
|
||||
|
||||
BusAcquireResult acquire(const BusAcquireRequest& request) override
|
||||
{
|
||||
const uint32_t start_ms = adapter_.nowMs();
|
||||
uint32_t timeout_ms = policy_.timeoutFor(request.policy);
|
||||
if (request.deadline_ms != 0)
|
||||
{
|
||||
const uint32_t remaining =
|
||||
static_cast<int32_t>(request.deadline_ms - start_ms) > 0
|
||||
? request.deadline_ms - start_ms
|
||||
: 0;
|
||||
if (remaining < timeout_ms)
|
||||
{
|
||||
timeout_ms = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
const bool acquired = adapter_.tryAcquire(timeout_ms);
|
||||
const uint32_t end_ms = adapter_.nowMs();
|
||||
|
||||
BusAcquireResult result{};
|
||||
result.status = acquired ? BusAcquireStatus::Acquired
|
||||
: (timeout_ms == 0 ? BusAcquireStatus::Busy
|
||||
: BusAcquireStatus::TimedOut);
|
||||
result.token.resource = request.resource;
|
||||
result.token.owner = request.command_id;
|
||||
result.token.acquired_ms = acquired ? end_ms : 0;
|
||||
result.token.valid = acquired;
|
||||
result.diagnostics.resource = request.resource;
|
||||
result.diagnostics.owner = adapter_.owner();
|
||||
result.diagnostics.command_id = request.command_id;
|
||||
result.diagnostics.wait_ms = end_ms - start_ms;
|
||||
result.diagnostics.policy = request.policy;
|
||||
|
||||
updateHealth(result.status, end_ms);
|
||||
return result;
|
||||
}
|
||||
|
||||
void release(const BusAccessToken& token) override
|
||||
{
|
||||
if (!token.valid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
adapter_.release();
|
||||
consecutive_timeouts_ = 0;
|
||||
if (health_.status == StorageHealthStatus::Slow ||
|
||||
health_.status == StorageHealthStatus::Recovering)
|
||||
{
|
||||
health_.status = StorageHealthStatus::Healthy;
|
||||
health_.last_error = 0;
|
||||
health_.last_transition_ms = adapter_.nowMs();
|
||||
}
|
||||
}
|
||||
|
||||
StorageHealthState health() const override
|
||||
{
|
||||
return health_;
|
||||
}
|
||||
|
||||
BusAccessPolicy selectPolicy(const RuntimeCommand& command) const
|
||||
{
|
||||
return policy_.select(command);
|
||||
}
|
||||
|
||||
private:
|
||||
void updateHealth(BusAcquireStatus status, uint32_t now_ms)
|
||||
{
|
||||
if (status == BusAcquireStatus::Acquired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
health_.last_transition_ms = now_ms;
|
||||
if (status == BusAcquireStatus::Unavailable)
|
||||
{
|
||||
health_.status = StorageHealthStatus::Unavailable;
|
||||
health_.last_error = -3;
|
||||
return;
|
||||
}
|
||||
|
||||
++consecutive_timeouts_;
|
||||
health_.last_error = status == BusAcquireStatus::TimedOut ? -2 : -1;
|
||||
health_.status = consecutive_timeouts_ >= 3 ? StorageHealthStatus::Degraded
|
||||
: StorageHealthStatus::Slow;
|
||||
}
|
||||
|
||||
IBusAdapter& adapter_;
|
||||
BusPolicyStrategy& policy_;
|
||||
StorageHealthState health_{};
|
||||
uint8_t consecutive_timeouts_ = 0;
|
||||
};
|
||||
|
||||
class IPlatformStorageAdapter
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
#pragma once
|
||||
|
||||
#include "sys/feedback_runtime.h"
|
||||
#include "sys/persistence_runtime.h"
|
||||
#include "sys/runtime_async.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
|
||||
namespace sys
|
||||
{
|
||||
namespace runtime
|
||||
{
|
||||
|
||||
class FakeClock
|
||||
{
|
||||
public:
|
||||
uint32_t now() const
|
||||
{
|
||||
return now_ms_;
|
||||
}
|
||||
|
||||
void advance(uint32_t ms)
|
||||
{
|
||||
now_ms_ += ms;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t now_ms_ = 0;
|
||||
};
|
||||
|
||||
class FakeCommandQueue : public ICommandQueue
|
||||
{
|
||||
public:
|
||||
bool enqueue(const RuntimeCommand& command) override
|
||||
{
|
||||
return queue_.enqueue(command);
|
||||
}
|
||||
|
||||
std::size_t cancel(uint32_t dedupe_key) override
|
||||
{
|
||||
return queue_.cancel(dedupe_key);
|
||||
}
|
||||
|
||||
bool popReady(uint32_t now_ms, RuntimeCommand& out) override
|
||||
{
|
||||
return queue_.popReady(now_ms, out);
|
||||
}
|
||||
|
||||
std::size_t size() const
|
||||
{
|
||||
return queue_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
FixedCommandQueue<32> queue_{};
|
||||
};
|
||||
|
||||
class FakeEventBus : public IEventSink,
|
||||
public IPersistenceEventSink,
|
||||
public IFeedbackEventSink
|
||||
{
|
||||
public:
|
||||
bool publish(const RuntimeEvent& event) override
|
||||
{
|
||||
return runtime_events_.publish(event);
|
||||
}
|
||||
|
||||
bool publish(const PersistenceEvent& event) override
|
||||
{
|
||||
if (persistence_count_ >= kMaxEvents)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
persistence_events_[persistence_count_++] = event;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool publish(const FeedbackEvent& event) override
|
||||
{
|
||||
if (feedback_count_ >= kMaxEvents)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
feedback_events_[feedback_count_++] = event;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t drain()
|
||||
{
|
||||
std::size_t count = 0;
|
||||
RuntimeEvent event{};
|
||||
while (runtime_events_.pop(event))
|
||||
{
|
||||
++count;
|
||||
}
|
||||
count += persistence_count_;
|
||||
count += feedback_count_;
|
||||
persistence_count_ = 0;
|
||||
feedback_count_ = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
std::size_t persistenceCount() const
|
||||
{
|
||||
return persistence_count_;
|
||||
}
|
||||
|
||||
std::size_t feedbackCount() const
|
||||
{
|
||||
return feedback_count_;
|
||||
}
|
||||
|
||||
const PersistenceEvent& persistenceEvent(std::size_t index) const
|
||||
{
|
||||
return persistence_events_[index];
|
||||
}
|
||||
|
||||
const FeedbackEvent& feedbackEvent(std::size_t index) const
|
||||
{
|
||||
return feedback_events_[index];
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kMaxEvents = 32;
|
||||
|
||||
FixedEventSink<32> runtime_events_{};
|
||||
PersistenceEvent persistence_events_[kMaxEvents]{};
|
||||
FeedbackEvent feedback_events_[kMaxEvents]{};
|
||||
std::size_t persistence_count_ = 0;
|
||||
std::size_t feedback_count_ = 0;
|
||||
};
|
||||
|
||||
class FakeStorageBackend : public IPlatformStorageAdapter,
|
||||
public IStoreSnapshotProvider,
|
||||
public IStoreStorageAdapter
|
||||
{
|
||||
public:
|
||||
void scriptDelay(const char* operation, uint32_t ms)
|
||||
{
|
||||
(void)operation;
|
||||
delay_ms_ = ms;
|
||||
}
|
||||
|
||||
void scriptFailure(const char* operation, int32_t error)
|
||||
{
|
||||
(void)operation;
|
||||
fail_ = true;
|
||||
error_ = error;
|
||||
}
|
||||
|
||||
PlatformStorageResult read(const PlatformStorageReadRequest& request) override
|
||||
{
|
||||
last_command_id_ = request.command_id;
|
||||
return platformResult(request.capacity);
|
||||
}
|
||||
|
||||
PlatformStorageResult write(const PlatformStorageWriteRequest& request) override
|
||||
{
|
||||
last_command_id_ = request.command_id;
|
||||
return platformResult(request.len);
|
||||
}
|
||||
|
||||
PlatformStorageResult list(const PlatformStorageListRequest& request) override
|
||||
{
|
||||
last_command_id_ = request.command_id;
|
||||
return platformResult(0);
|
||||
}
|
||||
|
||||
PlatformStorageResult flush(const PlatformStorageFlushRequest& request) override
|
||||
{
|
||||
last_command_id_ = request.command_id;
|
||||
return platformResult(0);
|
||||
}
|
||||
|
||||
StoreSnapshot snapshot(const char* store_key) override
|
||||
{
|
||||
(void)store_key;
|
||||
StoreSnapshot snapshot{};
|
||||
snapshot.data = snapshot_;
|
||||
snapshot.len = snapshot_len_;
|
||||
snapshot.valid = !fail_;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
StoreStorageResult write(const char* store_key,
|
||||
const uint8_t* bytes,
|
||||
std::size_t len) override
|
||||
{
|
||||
(void)store_key;
|
||||
(void)bytes;
|
||||
StoreStorageResult result{};
|
||||
result.ok = !fail_;
|
||||
result.bytes = result.ok ? len : 0;
|
||||
result.error = result.ok ? 0 : error_;
|
||||
++write_count_;
|
||||
return result;
|
||||
}
|
||||
|
||||
StoreStorageResult read(const char* store_key,
|
||||
uint8_t* bytes,
|
||||
std::size_t capacity,
|
||||
std::size_t& out_len) override
|
||||
{
|
||||
(void)store_key;
|
||||
(void)bytes;
|
||||
out_len = fail_ ? 0 : capacity;
|
||||
StoreStorageResult result{};
|
||||
result.ok = !fail_;
|
||||
result.bytes = out_len;
|
||||
result.error = result.ok ? 0 : error_;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t delayMs() const
|
||||
{
|
||||
return delay_ms_;
|
||||
}
|
||||
|
||||
uint32_t lastCommandId() const
|
||||
{
|
||||
return last_command_id_;
|
||||
}
|
||||
|
||||
std::size_t writeCount() const
|
||||
{
|
||||
return write_count_;
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformStorageResult platformResult(std::size_t bytes)
|
||||
{
|
||||
PlatformStorageResult result{};
|
||||
result.ok = !fail_;
|
||||
result.bytes = result.ok ? bytes : 0;
|
||||
result.error = result.ok ? 0 : error_;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t snapshot_[4] = {1, 2, 3, 4};
|
||||
std::size_t snapshot_len_ = sizeof(snapshot_);
|
||||
uint32_t delay_ms_ = 0;
|
||||
uint32_t last_command_id_ = 0;
|
||||
std::size_t write_count_ = 0;
|
||||
bool fail_ = false;
|
||||
int32_t error_ = -1;
|
||||
};
|
||||
|
||||
class FakeBusArbiter : public IBusArbiter
|
||||
{
|
||||
public:
|
||||
void scriptAcquire(BusAcquireStatus status)
|
||||
{
|
||||
scripted_status_ = status;
|
||||
}
|
||||
|
||||
BusAcquireResult acquire(const BusAcquireRequest& request) override
|
||||
{
|
||||
++acquire_count_;
|
||||
last_request_ = request;
|
||||
BusAcquireResult result{};
|
||||
result.status = scripted_status_;
|
||||
result.token.valid = scripted_status_ == BusAcquireStatus::Acquired;
|
||||
result.token.resource = request.resource;
|
||||
result.token.owner = request.command_id;
|
||||
result.token.acquired_ms = now_ms_;
|
||||
result.diagnostics.resource = request.resource;
|
||||
result.diagnostics.command_id = request.command_id;
|
||||
result.diagnostics.policy = request.policy;
|
||||
if (scripted_status_ != BusAcquireStatus::Acquired)
|
||||
{
|
||||
health_.status = StorageHealthStatus::Slow;
|
||||
health_.last_error = -1;
|
||||
health_.last_transition_ms = now_ms_;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void release(const BusAccessToken& token) override
|
||||
{
|
||||
if (token.valid)
|
||||
{
|
||||
++release_count_;
|
||||
}
|
||||
}
|
||||
|
||||
StorageHealthState health() const override
|
||||
{
|
||||
return health_;
|
||||
}
|
||||
|
||||
BusAcquireRequest diagnostics() const
|
||||
{
|
||||
return last_request_;
|
||||
}
|
||||
|
||||
void setNow(uint32_t now_ms)
|
||||
{
|
||||
now_ms_ = now_ms;
|
||||
}
|
||||
|
||||
std::size_t acquireCount() const
|
||||
{
|
||||
return acquire_count_;
|
||||
}
|
||||
|
||||
std::size_t releaseCount() const
|
||||
{
|
||||
return release_count_;
|
||||
}
|
||||
|
||||
private:
|
||||
BusAcquireStatus scripted_status_ = BusAcquireStatus::Acquired;
|
||||
BusAcquireRequest last_request_{};
|
||||
StorageHealthState health_{};
|
||||
uint32_t now_ms_ = 0;
|
||||
std::size_t acquire_count_ = 0;
|
||||
std::size_t release_count_ = 0;
|
||||
};
|
||||
|
||||
class FakeUiOwner : public IUiEffectSink
|
||||
{
|
||||
public:
|
||||
void assertNoBlockingCalls() const
|
||||
{
|
||||
assert(blocking_calls_ == 0);
|
||||
}
|
||||
|
||||
bool apply(const RuntimeUiEffect& effect) override
|
||||
{
|
||||
last_effect_ = effect;
|
||||
++effect_count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void tick()
|
||||
{
|
||||
++tick_count_;
|
||||
}
|
||||
|
||||
void recordBlockingCall()
|
||||
{
|
||||
++blocking_calls_;
|
||||
}
|
||||
|
||||
std::size_t effectCount() const
|
||||
{
|
||||
return effect_count_;
|
||||
}
|
||||
|
||||
private:
|
||||
RuntimeUiEffect last_effect_{};
|
||||
std::size_t effect_count_ = 0;
|
||||
std::size_t tick_count_ = 0;
|
||||
std::size_t blocking_calls_ = 0;
|
||||
};
|
||||
|
||||
class FakeFeedbackPresenter : public IFeedbackPresenter
|
||||
{
|
||||
public:
|
||||
bool present(const NoticeIntent& intent) override
|
||||
{
|
||||
if (count_ >= kMaxNotices)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
captured_[count_++] = intent;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t captured() const
|
||||
{
|
||||
return count_;
|
||||
}
|
||||
|
||||
const NoticeIntent& capturedNotice(std::size_t index) const
|
||||
{
|
||||
return captured_[index];
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kMaxNotices = 16;
|
||||
NoticeIntent captured_[kMaxNotices]{};
|
||||
std::size_t count_ = 0;
|
||||
};
|
||||
|
||||
class RuntimeHarness
|
||||
{
|
||||
public:
|
||||
FakeClock& clock()
|
||||
{
|
||||
return clock_;
|
||||
}
|
||||
|
||||
FakeCommandQueue& commands()
|
||||
{
|
||||
return commands_;
|
||||
}
|
||||
|
||||
FakeEventBus& events()
|
||||
{
|
||||
return events_;
|
||||
}
|
||||
|
||||
FakeStorageBackend& storage()
|
||||
{
|
||||
return storage_;
|
||||
}
|
||||
|
||||
FakeBusArbiter& bus()
|
||||
{
|
||||
return bus_;
|
||||
}
|
||||
|
||||
FakeUiOwner& ui()
|
||||
{
|
||||
return ui_;
|
||||
}
|
||||
|
||||
FakeFeedbackPresenter& feedbackPresenter()
|
||||
{
|
||||
return feedback_;
|
||||
}
|
||||
|
||||
void advance(uint32_t ms)
|
||||
{
|
||||
clock_.advance(ms);
|
||||
bus_.setNow(clock_.now());
|
||||
}
|
||||
|
||||
std::size_t drainUi()
|
||||
{
|
||||
ui_.tick();
|
||||
return events_.drain();
|
||||
}
|
||||
|
||||
void runUntilIdle(uint32_t step_ms = 10, uint32_t max_steps = 64)
|
||||
{
|
||||
for (uint32_t i = 0; i < max_steps; ++i)
|
||||
{
|
||||
advance(step_ms);
|
||||
if (drainUi() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
FakeClock clock_{};
|
||||
FakeCommandQueue commands_{};
|
||||
FakeEventBus events_{};
|
||||
FakeStorageBackend storage_{};
|
||||
FakeBusArbiter bus_{};
|
||||
FakeUiOwner ui_{};
|
||||
FakeFeedbackPresenter feedback_{};
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace sys
|
||||
@@ -40,6 +40,39 @@ class FakeUiEffectSink final : public sys::runtime::IUiEffectSink
|
||||
sys::runtime::RuntimeUiEffect last_effect{};
|
||||
};
|
||||
|
||||
class FakeBusAdapter final : public sys::runtime::IBusAdapter
|
||||
{
|
||||
public:
|
||||
bool acquire_ok = true;
|
||||
uint32_t now_ms = 100;
|
||||
uint32_t last_timeout_ms = 0;
|
||||
int acquire_count = 0;
|
||||
int release_count = 0;
|
||||
|
||||
bool tryAcquire(uint32_t timeout_ms) override
|
||||
{
|
||||
last_timeout_ms = timeout_ms;
|
||||
++acquire_count;
|
||||
now_ms += timeout_ms;
|
||||
return acquire_ok;
|
||||
}
|
||||
|
||||
void release() override
|
||||
{
|
||||
++release_count;
|
||||
}
|
||||
|
||||
uint32_t nowMs() const override
|
||||
{
|
||||
return now_ms;
|
||||
}
|
||||
|
||||
uint32_t owner() const override
|
||||
{
|
||||
return 77;
|
||||
}
|
||||
};
|
||||
|
||||
void test_priority_pop_order()
|
||||
{
|
||||
sys::runtime::FixedCommandQueue<4> queue;
|
||||
@@ -223,6 +256,45 @@ void test_event_to_ui_effect_bridge()
|
||||
assert(out.event_id == event.event_id);
|
||||
}
|
||||
|
||||
void test_storage_bus_arbiter_uses_policy_timeout()
|
||||
{
|
||||
FakeBusAdapter adapter;
|
||||
sys::runtime::DefaultBusPolicyStrategy policy;
|
||||
sys::runtime::StorageBusArbiter arbiter(adapter, policy);
|
||||
|
||||
sys::runtime::BusAcquireRequest request{};
|
||||
request.resource = 3;
|
||||
request.command_id = 9;
|
||||
request.policy = sys::runtime::BusAccessPolicy::InteractiveWorkerBounded;
|
||||
|
||||
const sys::runtime::BusAcquireResult result = arbiter.acquire(request);
|
||||
assert(result.status == sys::runtime::BusAcquireStatus::Acquired);
|
||||
assert(result.token.valid);
|
||||
assert(result.token.owner == 9);
|
||||
assert(adapter.last_timeout_ms == 2);
|
||||
assert(result.diagnostics.owner == 77);
|
||||
|
||||
arbiter.release(result.token);
|
||||
assert(adapter.release_count == 1);
|
||||
}
|
||||
|
||||
void test_storage_bus_arbiter_reports_degraded_after_timeouts()
|
||||
{
|
||||
FakeBusAdapter adapter;
|
||||
adapter.acquire_ok = false;
|
||||
sys::runtime::DefaultBusPolicyStrategy policy;
|
||||
sys::runtime::StorageBusArbiter arbiter(adapter, policy);
|
||||
|
||||
sys::runtime::BusAcquireRequest request{};
|
||||
request.policy = sys::runtime::BusAccessPolicy::BackgroundWorkerBounded;
|
||||
|
||||
assert(arbiter.acquire(request).status == sys::runtime::BusAcquireStatus::TimedOut);
|
||||
assert(arbiter.health().status == sys::runtime::StorageHealthStatus::Slow);
|
||||
assert(arbiter.acquire(request).status == sys::runtime::BusAcquireStatus::TimedOut);
|
||||
assert(arbiter.acquire(request).status == sys::runtime::BusAcquireStatus::TimedOut);
|
||||
assert(arbiter.health().status == sys::runtime::StorageHealthStatus::Degraded);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -234,5 +306,7 @@ int main()
|
||||
test_runtime_facade_submit_tick();
|
||||
test_runtime_facade_dedupe_cancel_policy();
|
||||
test_event_to_ui_effect_bridge();
|
||||
test_storage_bus_arbiter_uses_policy_timeout();
|
||||
test_storage_bus_arbiter_reports_degraded_after_timeouts();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#include "gps/track_runtime.h"
|
||||
#include "sys/feedback_runtime.h"
|
||||
#include "sys/persistence_runtime.h"
|
||||
#include "sys/runtime_harness.h"
|
||||
#include "ui_map_runtime/map_tiles/map_tile_async_runtime.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class MapCommandSink final : public ui::map_tiles::IMapTileCommandSink
|
||||
{
|
||||
public:
|
||||
bool enqueue(const ui::map_tiles::LoadTileCommand& command) override
|
||||
{
|
||||
if (count >= 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
commands[count++] = command;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t cancelGeneration(uint32_t generation) override
|
||||
{
|
||||
std::size_t removed = 0;
|
||||
ui::map_tiles::LoadTileCommand kept[4]{};
|
||||
std::size_t kept_count = 0;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (commands[i].runtime.generation == generation)
|
||||
{
|
||||
++removed;
|
||||
}
|
||||
else
|
||||
{
|
||||
kept[kept_count++] = commands[i];
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < kept_count; ++i)
|
||||
{
|
||||
commands[i] = kept[i];
|
||||
}
|
||||
count = kept_count;
|
||||
return removed;
|
||||
}
|
||||
|
||||
ui::map_tiles::LoadTileCommand commands[4]{};
|
||||
std::size_t count = 0;
|
||||
};
|
||||
|
||||
class MapUiSink final : public ui::map_tiles::IMapTileUiSink
|
||||
{
|
||||
public:
|
||||
bool applyTile(const ui::map_tiles::MapTileEvent& event) override
|
||||
{
|
||||
last = event;
|
||||
++count;
|
||||
return true;
|
||||
}
|
||||
|
||||
ui::map_tiles::MapTileEvent last{};
|
||||
std::size_t count = 0;
|
||||
};
|
||||
|
||||
class TrackEvents final : public gps::runtime::ITrackEventSink
|
||||
{
|
||||
public:
|
||||
bool publish(const gps::runtime::TrackEvent& event) override
|
||||
{
|
||||
if (count >= 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
events[count++] = event;
|
||||
return true;
|
||||
}
|
||||
|
||||
gps::runtime::TrackEvent events[8]{};
|
||||
std::size_t count = 0;
|
||||
};
|
||||
|
||||
class TrackFiles final : public gps::runtime::ITrackFileAdapter
|
||||
{
|
||||
public:
|
||||
bool open(uint32_t track_id) override
|
||||
{
|
||||
active_track = track_id;
|
||||
++open_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool append(uint32_t track_id,
|
||||
const gps::runtime::TrackPoint* points,
|
||||
std::size_t count) override
|
||||
{
|
||||
assert(track_id == active_track);
|
||||
assert(points != nullptr || count == 0);
|
||||
appended += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool flush(uint32_t track_id) override
|
||||
{
|
||||
assert(track_id == active_track);
|
||||
++flush_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool close(uint32_t track_id) override
|
||||
{
|
||||
assert(track_id == active_track);
|
||||
++close_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t list(uint32_t* track_ids, std::size_t capacity) override
|
||||
{
|
||||
if (track_ids && capacity > 0)
|
||||
{
|
||||
track_ids[0] = active_track;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t active_track = 0;
|
||||
std::size_t appended = 0;
|
||||
std::size_t open_count = 0;
|
||||
std::size_t flush_count = 0;
|
||||
std::size_t close_count = 0;
|
||||
};
|
||||
|
||||
ui::map_tiles::MapTileRef tileRef()
|
||||
{
|
||||
ui::map_tiles::MapTileRef ref{};
|
||||
ref.layer = ui::map_tiles::MapTileLayer::Osm;
|
||||
ref.z = 12;
|
||||
ref.x = 100;
|
||||
ref.y = 200;
|
||||
return ref;
|
||||
}
|
||||
|
||||
void test_map_tile_runtime_contract()
|
||||
{
|
||||
MapCommandSink commands;
|
||||
ui::map_tiles::MapTileAsyncRuntime async(commands);
|
||||
ui::map_tiles::MapTileStateMachine states;
|
||||
MapUiSink ui;
|
||||
ui::map_tiles::MapTileRuntime runtime(async, states, &ui);
|
||||
|
||||
ui::map_tiles::MapViewportPlan plan{};
|
||||
plan.generation = 7;
|
||||
plan.tile_count = 1;
|
||||
plan.tiles[0] = tileRef();
|
||||
assert(runtime.requestVisibleTiles(plan, 10) == 1);
|
||||
assert(commands.count == 1);
|
||||
|
||||
ui::map_tiles::MapTileEvent ready{};
|
||||
ready.kind = ui::map_tiles::MapTileEventKind::Ready;
|
||||
ready.generation = 7;
|
||||
ready.tile = tileRef();
|
||||
ui::map_tiles::MapTileRenderQueue render_queue;
|
||||
assert(runtime.handle(ready, render_queue));
|
||||
assert(ui.count == 1);
|
||||
assert(runtime.snapshot().ready_count == 1);
|
||||
}
|
||||
|
||||
void test_persistence_runtime_contract()
|
||||
{
|
||||
sys::runtime::RuntimeHarness harness;
|
||||
sys::runtime::DirtyStoreRegistry<4> registry;
|
||||
sys::runtime::DefaultPersistencePolicy policy;
|
||||
sys::runtime::PersistenceWorker worker(harness.storage(),
|
||||
harness.storage(),
|
||||
harness.bus(),
|
||||
harness.events(),
|
||||
policy);
|
||||
sys::runtime::PersistenceRuntime<4> runtime(registry,
|
||||
worker,
|
||||
harness.events(),
|
||||
policy);
|
||||
|
||||
assert(runtime.markDirty("nodes", 0));
|
||||
runtime.tick(100);
|
||||
assert(harness.storage().writeCount() == 0);
|
||||
runtime.tick(150);
|
||||
assert(harness.storage().writeCount() == 1);
|
||||
assert(harness.bus().acquireCount() == 1);
|
||||
assert(harness.events().persistenceCount() >= 3);
|
||||
}
|
||||
|
||||
void test_feedback_runtime_contract()
|
||||
{
|
||||
sys::runtime::RuntimeHarness harness;
|
||||
sys::runtime::FeedbackQueue<4> queue;
|
||||
sys::runtime::DefaultFeedbackPolicy policy;
|
||||
sys::runtime::FeedbackRuntime<4> runtime(queue,
|
||||
policy,
|
||||
harness.feedbackPresenter(),
|
||||
harness.events());
|
||||
|
||||
sys::runtime::NoticeIntent sent{};
|
||||
sent.category = sys::runtime::NoticeCategory::ChatDelivery;
|
||||
sent.severity = sys::runtime::NoticeSeverity::Success;
|
||||
sys::runtime::setNoticeMessage(sent, "Sent");
|
||||
sent.dedupe_key = 42;
|
||||
sent.created_at_ms = 10;
|
||||
assert(runtime.post(sent));
|
||||
assert(runtime.post(sent));
|
||||
assert(runtime.drainToUi(20) == 1);
|
||||
assert(harness.feedbackPresenter().captured() == 1);
|
||||
assert(harness.feedbackPresenter().capturedNotice(0).duration_ms == 1400);
|
||||
}
|
||||
|
||||
void test_track_runtime_contract()
|
||||
{
|
||||
sys::runtime::RuntimeHarness harness;
|
||||
TrackFiles files;
|
||||
TrackEvents events;
|
||||
gps::runtime::DefaultTrackFlushPolicy policy;
|
||||
gps::runtime::TrackStorageWorker worker(files, harness.bus(), events, policy);
|
||||
gps::runtime::TrackPointBuffer<8> points;
|
||||
gps::runtime::TrackStateMachine states;
|
||||
gps::runtime::TrackRuntime<8> runtime(points, states, policy, worker, events);
|
||||
|
||||
assert(runtime.startNewTrack(123, 0));
|
||||
runtime.tick(1);
|
||||
assert(events.count == 1);
|
||||
runtime.handle(events.events[0]);
|
||||
assert(states.state() == gps::runtime::TrackRecorderStatus::Recording);
|
||||
|
||||
gps::runtime::TrackPoint point{};
|
||||
point.latitude = 1.0;
|
||||
point.longitude = 2.0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
point.timestamp_ms = static_cast<uint32_t>(i);
|
||||
assert(runtime.appendPoint(point, 10 + static_cast<uint32_t>(i)));
|
||||
}
|
||||
runtime.tick(30);
|
||||
assert(files.appended == 8);
|
||||
assert(files.flush_count == 1);
|
||||
}
|
||||
|
||||
void test_runtime_harness_keeps_ui_drain_separate()
|
||||
{
|
||||
sys::runtime::RuntimeHarness harness;
|
||||
harness.advance(5);
|
||||
harness.ui().assertNoBlockingCalls();
|
||||
assert(harness.drainUi() == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_map_tile_runtime_contract();
|
||||
test_persistence_runtime_contract();
|
||||
test_feedback_runtime_contract();
|
||||
test_track_runtime_contract();
|
||||
test_runtime_harness_keeps_ui_drain_separate();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user