Skip to content

feat: enhance AtemSocket to handle caching#191

Open
milux wants to merge 3 commits into
Sofie-Automation:mainfrom
milux:main
Open

feat: enhance AtemSocket to handle caching#191
milux wants to merge 3 commits into
Sofie-Automation:mainfrom
milux:main

Conversation

@milux

@milux milux commented Feb 23, 2026

Copy link
Copy Markdown

About the Contributor

This PR is a personal contribution to the sofie-atem-connection library in order to improve overall efficiency.
Fixes #190.

Type of Contribution

This is a:

Bug fix / Feature

Current Behavior

As discussed in #190, highly frequent command batches of TimeCommands followed by other redundant commands with identical contents cause significant CPU load, even when ATEM devices are not actively "doing" anything in particular.

New Behavior

The CPU load was (again) cut down approximately by half for affected ATEM devices.
This is achieved by two means primarily:

  • A full copy of the incoming UDP packet's serialized commands was substituted by a "normalization function" which creates a view on the passed memory instead of copying it.
  • The serialized representation of command batches starting with a TimeCommand are (partially, after the TimeCommand itself) cached. Iff the next batch also starts with a time command and otherwise equals the previous batch (byte-wise) entirely. Only the TimeCommand is emitted and the remaining contents are dropped without (redundant) deserialization and further processing.

Testing Instructions

I tried to provide full (jest) test coverage for all added and modified code snippets.

I took precautions to make sure my caching strategy is sound by always flushing/updating the cached copy if a command batch does either not start with a TimeCommand or differs by content or length after the TimeCommand.

My optimization should be valid iff the following assumptions hold:

  • Processing of commands is idempotent, i.e., processing the same batches of commands multiple times always yields the same state.
  • Any message changing internal state managed by command messages passes through _parseCommands, i.e., there is no "side channel" that may cause state changes that would normally be overwritten by the commands received.

If I am mistaken about any of those assumptions, my PR may introduce subtile semantic errors and should not be merged as is.

Other Information

Since this is my first contribution to this lib, I want to kindly ask you carefully reviewing my changes and see if they fit your expectations.
Specifically, please see if my assumptions above hold. If they do, I am optimistic that this PR can be safely merged.

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

Overview

Implements an optimization to reduce CPU load from frequent, redundant ATEM command batches (notably TimeCommand-heavy traffic). Adds payload normalization to avoid buffer copies and a caching mechanism to skip redundant deserialization of command batch remainders when batches start with a TimeCommand.

Key Changes

Core Implementation (src/lib/atemSocket.ts)

  • Exports new type: ThreadedPayload = Buffer | Uint8Array | { type: 'Buffer'; data: number[] }.
  • Payload normalization:
    • Adds private _normalizePayload(payload: ThreadedPayload) to convert supported payload variants to a native Buffer view without making full copies; returns undefined for invalid shapes.
    • Applies normalization in the threaded-socket onCommandsReceived callback before parsing.
  • TimeCommand remainder caching:
    • Adds private _lastTimeCommandRemainder: Buffer | undefined to cache the bytes following a leading TimeCommand.
    • _parseCommands() tracks the first command; if it is a TimeCommand the remainder is compared against the cached remainder. If the remainder equals the cache, only the TimeCommand is processed and parsing stops. If different, the remainder is cached via Buffer.from(remainder). If the first command is not a TimeCommand, the cache is cleared.
    • Adds stricter length validation for commands (length < 8 || length > buffer.length).
  • Connection lifecycle:
    • Resets _lastTimeCommandRemainder on connect and disconnect.
  • Error/debug behavior preserved for invalid payloads and deserialization failures.

Public API / Type Changes

  • ThreadedPayload exported from the AtemSocket module.
  • Threaded socket child callback signature updated to accept ThreadedPayload; tests and mocks updated accordingly.

Tests (src/lib/tests/atemSocket.spec.ts)

  • Tests adapted to use ThreadedPayload and the normalization path.
  • New/updated tests cover:
    • TimeCommand remainder deduplication and cache-update/clear behavior.
    • Payload normalization variants (Uint8Array, { type: 'Buffer'; data: number[] }) and invalid payload handling.
  • Mocked AtemSocketChild and ThreadedClassManager usage adjusted to the new callback types.

Implications & Assumptions

  • Correctness depends on command processing being idempotent and all state-changing messages flowing through _parseCommands() (author-documented assumptions). Reviewers should validate these assumptions before merging.
  • Cache semantics: only applies when the first command is a TimeCommand; flushed whenever a batch starts with a different command or the remainder differs in content or length.

Performance Impact

  • Reduces memory allocations by creating Buffer views instead of full copies and avoids redundant deserialization/processing for repeated TimeCommand-led batches; the author reports roughly a 50% CPU reduction in affected scenarios.

@coderabbitai

coderabbitai Bot commented Feb 23, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22515f7 and e39d728.

📒 Files selected for processing (1)
  • src/lib/atemSocket.ts

Walkthrough

Exports a new ThreadedPayload union, normalizes incoming threaded payloads to Buffer, and updates command parsing to deduplicate TimeCommand remainders across packet boundaries. onCommandsReceived signature and tests were updated to accept ThreadedPayload. Connection lifecycle resets the TimeCommand remainder cache.

Changes

Cohort / File(s) Summary
Payload Type & Normalization
src/lib/atemSocket.ts
Adds exported `ThreadedPayload = Buffer
TimeCommand Remainder Deduplication
src/lib/atemSocket.ts
Introduces private _lastTimeCommandRemainder state; _parseCommands tracks isFirstCommand and compares/caches the TimeCommand remainder to avoid duplicate processing across packet batches; cache cleared on connect/disconnect.
Parsing Robustness & Validation
src/lib/atemSocket.ts
Strengthens length checks (rejects length < 8 or length > buffer.length), handles deserialization failures with error/debug messages, and integrates payload normalization into parsing flow.
Public API & Tests
src/lib/atemSocket.ts, src/lib/__tests__/atemSocket.spec.ts
Changes onCommandsReceived signature from (payload: Buffer, packetId: number) to (payload: ThreadedPayload, packetId: number); updates test mocks/specs to import/use ThreadedPayload; adds tests for remainder deduplication, payload normalization variants, and invalid payload handling.

Sequence Diagram(s)

sequenceDiagram
participant Client as Client
participant Socket as AtemSocket
participant Normalizer as Normalizer
participant Parser as Parser
participant Cache as TimeCache

Client->>Socket: send payload (ThreadedPayload, packetId)
Socket->>Normalizer: _normalizePayload(payload)
Normalizer-->>Socket: Buffer or undefined
alt payload invalid
    Socket-->>Client: drop / emit error
else payload valid
    Socket->>Parser: _parseCommands(buffer, packetId)
    Parser->>Cache: check isFirstCommand & remainder
    alt first is TimeCommand and remainder == cached
        Cache-->>Parser: match -> process first only, stop parsing
    else first is TimeCommand and remainder != cached
        Cache-->>Parser: cache remainder -> stop parsing
    else not TimeCommand
        Cache-->>Parser: clear cached remainder
    end
    Parser-->>Socket: parsed commands
    Socket-->>Client: deliver commands / callbacks
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I chewed the bytes from odd-shaped trays,
Turned threaded crumbs to buffers in neat arrays.
TimeCommand echoes now hop just once and then stop,
Remainders cached and cleared on each connect or drop.
I nibble bugs away — parsing now on the hop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly and specifically describes the main enhancement: adding caching to AtemSocket to optimize performance.
Linked Issues check ✅ Passed PR implements all key requirements from #190: caching TimeCommand-based batches to reduce redundant processing, normalizing payloads to avoid copies, and achieving the stated CPU reduction objective.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives: ThreadedPayload type support, remainder caching logic, payload normalization, and updated test coverage are all directly addressing the CPU optimization goal from #190.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/atemSocket.ts`:
- Line 44: The cached buffer _lastTimeCommandRemainder must be cleared on
connection lifecycle changes to avoid stale-remainder matches; update the
AtemSocket.connect and AtemSocket.disconnect methods to reset
_lastTimeCommandRemainder = undefined (and likewise reset any other "remainder"
buffer fields you have, e.g., packet/command remainder variables) so no leftover
bytes from a previous session can be matched against a new device/session.
- Around line 215-233: The bug is that isFirstCommand stays true if command
deserialization fails or is unknown, causing a later command to be treated as
the "first" and wrongly apply the TimeCommand remainder logic; to fix, ensure
isFirstCommand is set to false for every command slot immediately after
attempting to read/deserialise a command (regardless of success), so move the
isFirstCommand = false assignment out of the TimeCommand-success branch and into
the per-command processing loop right after command identification/attempted
deserialization; update logic around TimeCommand, _lastTimeCommandRemainder,
parsedCommands and any early-break paths so the remainder cache is only applied
when isFirstCommand was true for that slot and then cleared/updated
consistently.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd53b77 and 9fc0a3f.

📒 Files selected for processing (2)
  • src/lib/__tests__/atemSocket.spec.ts
  • src/lib/atemSocket.ts

Comment thread src/lib/atemSocket.ts
Comment thread src/lib/atemSocket.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/lib/atemSocket.ts (2)

56-77: connect() still does not reset _lastTimeCommandRemainder — stale cache via the restarted path.

The restarted event (line 176) calls this.connect() directly without going through disconnect(), so a stale remainder from the previous session can survive into the new connection and silently suppress real state-update commands on the first matching batch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/atemSocket.ts` around lines 56 - 77, The connect() method must clear
the stale command remainder so a leftover _lastTimeCommandRemainder from a
previous session doesn't suppress new commands; update connect() (the method
invoked by the restarted event) to reset/empty the _lastTimeCommandRemainder
before establishing a new socket (e.g., at the start of connect() or right after
setting this._address/this._port) so the restarted path behaves the same as
disconnect()+connect().

239-243: isFirstCommand still not reset for the unknown-command path.

isFirstCommand = false (line 240) is inside the if (cmdConstructor && …) branch, so if the first command is unrecognised (falls into the else at line 241), isFirstCommand remains true and the second command inherits first-command status, opening the TimeCommand-cache logic to misapplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/atemSocket.ts` around lines 239 - 243, The flag isFirstCommand is
only cleared inside the recognized-command branch, so if the very first incoming
command is unrecognized the flag remains true and mislabels the next command;
update the handler around isFirstCommand (the code that checks cmdConstructor
and emits 'debug' for unknown commands) to always set isFirstCommand = false
after processing any command path (either move the assignment after the if/else
or add the assignment inside the else branch) so the first-command state is
cleared regardless of whether the command was recognized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/atemSocket.ts`:
- Around line 151-158: Change the callback parameter type from Buffer to the
union type used at runtime (ThreadedPayload) where the IPC message handler is
declared so TypeScript matches reality; update the async handler signature (the
function that calls this._normalizePayload and this._parseCommands) to accept
payload: ThreadedPayload, and ensure ThreadedPayload is imported or referenced
in the file so the call to _normalizePayload(payload) and subsequent
this._parseCommands(normalizedPayload) type-check correctly.

---

Duplicate comments:
In `@src/lib/atemSocket.ts`:
- Around line 56-77: The connect() method must clear the stale command remainder
so a leftover _lastTimeCommandRemainder from a previous session doesn't suppress
new commands; update connect() (the method invoked by the restarted event) to
reset/empty the _lastTimeCommandRemainder before establishing a new socket
(e.g., at the start of connect() or right after setting
this._address/this._port) so the restarted path behaves the same as
disconnect()+connect().
- Around line 239-243: The flag isFirstCommand is only cleared inside the
recognized-command branch, so if the very first incoming command is unrecognized
the flag remains true and mislabels the next command; update the handler around
isFirstCommand (the code that checks cmdConstructor and emits 'debug' for
unknown commands) to always set isFirstCommand = false after processing any
command path (either move the assignment after the if/else or add the assignment
inside the else branch) so the first-command state is cleared regardless of
whether the command was recognized.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc0a3f and 1a02279.

📒 Files selected for processing (1)
  • src/lib/atemSocket.ts

Comment thread src/lib/atemSocket.ts Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/atemSocket.ts`:
- Around line 240-244: The first-command state isn't cleared when a command is
unrecognized: ensure isFirstCommand and the cached _lastTimeCommandRemainder are
reset regardless of whether a cmdConstructor exists or deserialization succeeds;
specifically, move or duplicate the assignment isFirstCommand = false (and set
this._lastTimeCommandRemainder = undefined) out of the successful-deserialize
branch so that the else branch that emits `Unknown command ${name} (${length}b)`
also clears both flags/caches, preventing stale TimeCommand remainder from
affecting subsequent commands/batches.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a02279 and 9fa1007.

📒 Files selected for processing (1)
  • src/lib/atemSocket.ts

Comment thread src/lib/atemSocket.ts Outdated
@milux

milux commented Feb 24, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
src/lib/atemSocket.ts (1)

217-234: ⚠️ Potential issue | 🟡 Minor

TimeCommand remainder-caching logic is sound, but _lastTimeCommandRemainder is not invalidated when the first command is unknown or fails to deserialize.

When the first command in a batch is unrecognized (line 240–241) or its deserialization throws (line 237–238), the isFirstCommand block (lines 217–234) is never entered, so _lastTimeCommandRemainder silently persists from a previous batch. A later batch whose first command is a TimeCommand could then match against that stale remainder and incorrectly skip processing.

In practice this is a narrow edge case (unknown first commands are unusual, and the byte comparison provides a safety net), but for correctness it would be cleaner to clear the cache whenever the first-command slot is consumed without entering the TimeCommand path.

Proposed fix — clear cache for unknown / failed first commands
 			} else {
 				this.emit('debug', `Unknown command ${name} (${length}b)`)
 			}

 			// Always clear the first command flag after processing the first command.
+			if (isFirstCommand) {
+				// First command was unknown or deserialization failed — invalidate the cache.
+				this._lastTimeCommandRemainder = undefined
+			}
 			isFirstCommand = false

This works because whenever the code does reach the isFirstCommand block inside the try (line 217), it either caches a new remainder or clears it explicitly, and then falls through here with isFirstCommand already consumed. Adding this guard catches only the paths that bypassed the inner block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/atemSocket.ts` around lines 217 - 234, The _lastTimeCommandRemainder
cache can persist when the first-command slot is consumed by an unknown command
or when deserialization throws, so update the branches that consume the first
command but bypass the isFirstCommand TimeCommand logic to explicitly clear this
cache: inside the unknown-command branch (where the code currently handles
unrecognized commands) and inside the catch block that handles deserialization
errors, set this._lastTimeCommandRemainder = undefined so stale remainders
cannot later cause a TimeCommand batch to be skipped; refer to the
isFirstCommand flag, TimeCommand class check, and the _lastTimeCommandRemainder
field to locate the exact places to clear the cache.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/atemSocket.ts`:
- Around line 217-234: The _lastTimeCommandRemainder cache can persist when the
first-command slot is consumed by an unknown command or when deserialization
throws, so update the branches that consume the first command but bypass the
isFirstCommand TimeCommand logic to explicitly clear this cache: inside the
unknown-command branch (where the code currently handles unrecognized commands)
and inside the catch block that handles deserialization errors, set
this._lastTimeCommandRemainder = undefined so stale remainders cannot later
cause a TimeCommand batch to be skipped; refer to the isFirstCommand flag,
TimeCommand class check, and the _lastTimeCommandRemainder field to locate the
exact places to clear the cache.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9fa1007 and 22515f7.

📒 Files selected for processing (1)
  • src/lib/atemSocket.ts

@milux

milux commented Mar 2, 2026

Copy link
Copy Markdown
Author

@Julusian CodeRabbit was initially unhappy with my cache handling in some edge cases. Had some more iterations but think it is very robust now.
Would be great if you could have a look at it.

@Julusian

Julusian commented Mar 2, 2026

Copy link
Copy Markdown
Member

could you send me an xml export of your atem? Im not sure what I need to change to reproduce this, the old trigger doesnt appear to cause this anymore.

I'm currently wondering whether the risk (and minor complexity) this introduces is worth the performance gain, vs potentially finding another solution. I'm a little worried about the potential for causing something to be lost if it gets dumped at the end of the sequence. Maybe a bit cautious, I can't think of what could be there that could happen in 2 frames in a row.

I think this will be invalidated unnecessarily at times (eg, when theres a cut, that gets put after the TimeCommand in the same packet), but thats fine, as itll only be 2 packets of that.
When fairlight audio levels are being streamed, the TimeCommand appears to move to be in the middle/end of a packet, which will cause it to bypass this cache, but thats fine too and probably negligable compared to the cost of the rest of the packets.

I'm also wondering how much actual performance gain this gives, as parsing those commands cant be all that much work surely. its a bit of extra parsing of the buffers and object construction, but we are swallowing the change when it is doing nothing, so it shouldnt be leaving the bounds of the library code.
So I would like to understand where the cost lies

@milux

milux commented Mar 4, 2026

Copy link
Copy Markdown
Author

could you send me an xml export of your atem? Im not sure what I need to change to reproduce this, the old trigger doesnt appear to cause this anymore.

Funny, I just realized from the Source Code you referenced below that this issue on our device series has been known (and documented plus partially mitigated by you) since 2023. 😆

I created a XML export and can send it via any channel of your choice, but I'm 99 % sure that it won't provide you any helpful insight.

I'm currently wondering whether the risk (and minor complexity) this introduces is worth the performance gain, vs potentially finding another solution. I'm a little worried about the potential for causing something to be lost if it gets dumped at the end of the sequence. Maybe a bit cautious, I can't think of what could be there that could happen in 2 frames in a row.

I think this will be invalidated unnecessarily at times (eg, when theres a cut, that gets put after the TimeCommand in the same packet), but thats fine, as itll only be 2 packets of that. When fairlight audio levels are being streamed, the TimeCommand appears to move to be in the middle/end of a packet, which will cause it to bypass this cache, but thats fine too and probably negligable compared to the cost of the rest of the packets.

Right, it will be invalidated unnecessarily often times because I implemented it as defensive as possible to get the risk of error down to the absolute minimum.

As stated, the cache is flushed iff (equivalence relation!) the sequence does not start with a TimeCommand or otherwise differs in any byte from the previous batch/frame in any way. This is very effective on the buggy Constellation HD devices and costs basically nothing since the cached buffer "is already there".

The one and only risk I cannot safely rule out is "external state mutation" between frames with identical data, i.e., whether there is some code in the lib that could cause a state change which is not reflected by a command from the ATEM right after and might remain stale due to skipping of cached commands.

I'm also wondering how much actual performance gain this gives, as parsing those commands cant be all that much work surely. its a bit of extra parsing of the buffers and object construction, but we are swallowing the change when it is doing nothing, so it shouldnt be leaving the bounds of the library code. So I would like to understand where the cost lies

I totally agree with you in every part but for the "as parsing those commands cant be all that much work surely" assumption.

TBH, I thought so too, until I did the profiling and noticed that even this parsing and checking of 32 unspectacular MultiViewerSourceUpdateCommand instances against the current state is causing significant load when run 50 times a second.

I have to speculate about precise reasons here, but I assume that something of the comparison logic might work against caches or the prediction units of the CPU. Further, I have observed heavy fluctuation of the heap utilization before my caching patch which almost entirely vanished. That actually makes perfect sense since about 1.6k MultiViewerSourceUpdateCommand objects per second were flooding the heap previously.

@Julusian

Copy link
Copy Markdown
Member

I haven't entirely forgotten about this, but I'm still not 100% sure on if it introduces risk of error or not. With the range of users, that makes me nervous.

Maybe this could be gated behind a boolean on AtemOptions? That way it will be opt-in and it can get some real world testing before deciding to enable it by default in a future version.

Or, I know we don't have the structure in place, but maybe this should be gated on models that we know do or could have this issue?

@milux

milux commented Apr 30, 2026

Copy link
Copy Markdown
Author

I haven't entirely forgotten about this, but I'm still not 100% sure on if it introduces risk of error or not. With the range of users, that makes me nervous.

Maybe this could be gated behind a boolean on AtemOptions? That way it will be opt-in and it can get some real world testing before deciding to enable it by default in a future version.

Or, I know we don't have the structure in place, but maybe this should be gated on models that we know do or could have this issue?

Both approaches sound reasonable to me.
Thank you for keeping that on your radar.
The last benchmarks on better hardware didn't show the same effect strength anyway, but I would still appreciate this being included.

If you want me to implement either one of that approaches, please tell me what I shall do.
If you have some time to quickly code the flag or device-dependent behavior yourself - even better. :)

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.

Bug Report: High CPU load with 2 M/E Constellation HD

2 participants