Skip to content

V2 - Increase frametime accuracy#126

Open
davepl wants to merge 2 commits into
mainfrom
v2
Open

V2 - Increase frametime accuracy#126
davepl wants to merge 2 commits into
mainfrom
v2

Conversation

@davepl

@davepl davepl commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Was an integer number of millis before, now a proper timespan.
Adds the StockBanner scrolling ticker effect. Consumes the V2 stock API.

: c;
}

static constexpr array<uint8_t, 7> GlyphRows(char c)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a while to get this constexpr - but it was worth it :-)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves frame timing accuracy by switching feature frame generation to use an explicit system_clock::time_point, expands socket diagnostics, and adds a new scrolling Stock Banner effect that consumes the V2 stock API.

Changes:

  • Update LED feature frame headers to use an explicit display timestamp and improve frametime scheduling in EffectsManager.
  • Add new Stock Banner scrolling ticker effect and expose it via effect serialization/catalog.
  • Extend socket channel diagnostics (failed connect count + last socket error) and improve server shutdown handling.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
webserver.h Makes server wait/shutdown more robust by guarding invalid futures and logging exceptions.
main.cpp Clarifies startup logging by separating dashboard vs API endpoints.
interfaces.h Updates feature frame API to accept a display timestamp; adds new socket diagnostic getters.
ledfeature.h Implements timestamped frame generation and fixes reversed multi-row pixel mapping.
effectsmanager.h Improves frame pacing precision and passes computed display timestamps into features.
socketchannel.h Adds connection failure diagnostics and adjusts frame send batching logic and response aging check.
apihelpers.h Ensures sockets are restarted/stopped alongside canvas effect start/stop flows.
apihelpers.cpp Registers StockBanner in the effect catalog for the web UI.
effects/stockbannereffect.h Introduces the StockBanner effect with HTTP quote fetching and rendering.
secrets.h.example Adds configuration placeholder for the realtime quote key used by StockBanner.
tests/tests.cpp Adds a unit test validating reversed multi-row feature pixel mirroring; provides test logger/static defs.
tests/Makefile Adds link libs needed by new/updated dependencies used in tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread socketchannel.h
Comment on lines +480 to 490
size_t tempCount = 0;
size_t tempBytes = 0;
auto queueCopy = _frameQueue;
while (!queueCopy.empty() && tempCount < kMaxBatchSize)
{
tempBytes += queueCopy.front().size();
tempCount++;
queueCopy.pop();
}

combinedBuffer.reserve(tempBytes);
Comment on lines +298 to +301
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;


try
{
const string path = "/?ticker=" + symbol + "&v=2&profile=hub75&points=1";
Comment on lines +531 to +536
const string symbol = _quotes[*index].symbol;
_lastRequestStart = now;
_pendingFetch = async(launch::async, [this, symbol]()
{
return FetchQuote(symbol);
});
#include <utility>
#include <vector>

class StockBanner : public LEDEffectBase

@rbergen rbergen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review findings from reading the full diff.

uint32_t _stripHeight = 0;
bool _stripDirty = true;

future<FetchResult> _pendingFetch;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking destructor on std::async future. When StockBanner is destroyed while a fetch is in progress (e.g. because the effect is stopped), the destructor of a future obtained from std::async(launch::async, ...) blocks until the async task completes — up to _requestTimeout (1500 ms). Since Stop() can be called from the effects manager thread, this stalls the whole render loop. Consider using a detached thread with a shared cancellation flag, or storing the future in an optional and resetting it under a condition that allows early exit.


try
{
const string path = "/?ticker=" + symbol + "&v=2&profile=hub75&points=1";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No URL-encoding on user-supplied symbols. Ticker symbols from the JSON config are concatenated directly into the query string. A value like FOO&bar=1 would silently corrupt the request. Stock tickers are normally alphanumeric, but since these come from user config it's worth at least validating they match [A-Z0-9.\-]+ before building the URL.

const int bottomScale = LargestTextScale(priceText + (changeText.empty() ? "" : " " + changeText),
static_cast<int>(_compactQuoteWidth) - 8,
bottomHeight - 2);
const int width = static_cast<int>(max<uint32_t>({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tile width calculated twice. RebuildStrip computes each tile's width here to sum up the total strip width, and then RenderQuoteTile (line 576) recomputes it independently. If those two calculations ever diverge — e.g. due to a future edit — tiles will misalign. Consider extracting a ComputeTileWidth(const Quote&) helper that both callers use.

{
(void) deltaTime;

EnsureQuoteList();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnsureQuoteList() runs O(n²) work every frame. At the end of EnsureQuoteList, _symbols is always repopulated from _quotes, so it is never empty after the first call. That means the reconciliation loop (with its inner find_if over _quotes) runs on every Update() call. A _symbolsDirty flag that from_json sets and EnsureQuoteList clears would make this a no-op on the hot path.

Comment thread effectsmanager.h
auto nextFrameTime = steady_clock::now();
const auto effectiveFps = max<uint16_t>(1, _fps);
const auto frameDuration = duration_cast<steady_clock::duration>(duration<double>(1.0 / effectiveFps));
const auto effectDelta = duration_cast<milliseconds>(frameDuration + 500us);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic 500 µs offset is undocumented. effectDelta is passed as deltaTime to every effect's Update(). It is now a fixed value rather than the actual elapsed time, and the 500 µs padding has no explanation. Effects that use deltaTime for physics or animation will receive a slightly inflated constant every frame. Please document why 500 µs was chosen (scheduler wake-up margin? rounding buffer?).

Comment thread socketchannel.h
if (tempBytes > 0)
size_t tempCount = 0;
size_t tempBytes = 0;
auto queueCopy = _frameQueue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queueCopy allocation is vestigial with kMaxBatchSize = 1. The copy of the whole queue exists only to compute tempBytes for combinedBuffer.reserve(). With batch size 1, this copies at most one item to determine one frame's size — a combinedBuffer.reserve(_frameQueue.front().size()) would do the same without the copy.

Comment thread apihelpers.h
{
for (auto &feature : canvas->Features())
{
feature->Socket()->Stop();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Socket restart may block while holding the canvas mutex. ApplyCanvasesRequest holds the controller mutex during this callback. Socket()->Stop() followed by Socket()->Start() can block for up to reconnectDelay (1 s) if the socket is mid-connect. That would stall any concurrent HTTP request handler waiting on the same mutex. Please verify Stop() returns promptly, or move the socket lifecycle calls outside the mutex.

@rbergen rbergen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR suffers from a more-involved-than-usual merge conflict, almost certainly caused by a number of PRs from Robert's hand that I recently merged.

I can take a shot at resolving them if you'd like me to - I went through the merged PRs fairly recently so I still have some memories about which changes were made why.

@davepl

davepl commented Jun 28, 2026 via email

Copy link
Copy Markdown
Collaborator Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants