Skip to content

Comments

perf: recording performance optimizations#83

Open
fank wants to merge 7 commits intomainfrom
worktree-keen-elm-lips
Open

perf: recording performance optimizations#83
fank wants to merge 7 commits intomainfrom
worktree-keen-elm-lips

Conversation

@fank
Copy link
Member

@fank fank commented Feb 23, 2026

Summary

  • Cache vehicle/marker exclusion lists at init instead of parsing per entity/event
  • Fix broken unitData state deduplication (wrong variable name + frame number in comparison) — highest impact change, eliminates redundant extension calls for dead/stationary units
  • Cache player scores and unit type recalculation to skip redundant work when values haven't changed
  • Replace count anti-pattern with findIf for early-exit in APC classification
  • Replace chained select with single-pass counting in telemetry loop

Test Plan

  • hemtt build passes on every commit (44 SQF files compiled, 3 PBOs built)
  • Verify recording playback produces correct data in a live mission
  • Confirm dead/stationary units no longer send redundant :SOLDIER:STATE: calls

fank added 7 commits February 23, 2026 23:47
…ame from comparison

The state comparison at captureLoop:170 never worked due to two bugs:
1. Read from "unitData" but wrote to "OCAP_unitData" (QGVARMAIN mismatch)
2. Frame number at index 8 changed every frame, making comparison always unequal

Fix uses a 0 placeholder for frame during comparison and stores a deep copy.
This eliminates redundant extension calls for unchanged units (dead, stationary).
@gemini-code-assist
Copy link

Summary of Changes

Hello @fank, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces several performance optimizations to the recording system. By caching frequently accessed exclusion lists, fixing a critical unit data deduplication bug, and optimizing data collection loops, the changes aim to reduce CPU overhead and network traffic, particularly for dead or stationary units, leading to a more efficient and responsive recording process.

Highlights

  • Exclusion List Caching: Vehicle and marker exclusion lists are now cached during initialization to prevent repeated parsing, improving performance.
  • Unit Data Deduplication Fix: A critical bug in unit data state deduplication was corrected, significantly reducing redundant extension calls for inactive units.
  • Player Scores & Unit Type Caching: Implemented caching for player scores and unit type recalculations to avoid unnecessary work when these values remain unchanged.
  • Optimized APC Classification: An inefficient count anti-pattern was replaced with findIf for early-exit logic in APC classification, enhancing efficiency.
  • Telemetry Loop Refactoring: Streamlined telemetry data collection by replacing chained select operations with single-pass counting loops, reducing processing overhead.
Changelog
  • addons/recorder/fnc_captureLoop.sqf
    • Introduced caching for unit weapons and player scores to reduce redundant calculations.
    • Fixed a bug in unit data deduplication by correcting the variable name and adjusting the frame number comparison logic.
    • Replaced dynamic parsing of exclusion lists with pre-cached global variables.
  • addons/recorder/fnc_handleMarkers.sqf
    • Modified marker handling to use a globally cached exclusion list instead of parsing it repeatedly.
  • addons/recorder/fnc_init.sqf
    • Added initialization for global variables excludeKindList, excludeClassList, and excludeMarkerList to cache exclusion settings.
  • addons/recorder/fnc_isKindOfApc.sqf
    • Refactored the APC classification logic to use findIf for improved performance.
  • addons/recorder/fnc_telemetryLoop.sqf
    • Optimized telemetry data collection by consolidating multiple select and count calls into single-pass iteration and direct counting.
Activity
  • The hemtt build process has successfully passed on all commits.
  • Verification of recording playback and confirmation of reduced redundant :SOLDIER:STATE: calls for dead/stationary units are pending.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces several performance optimizations by caching frequently accessed data and refactoring loops for efficiency. Key changes include caching vehicle/marker exclusion lists, fixing unit data deduplication, caching player scores and unit types, and optimizing array iterations. These changes align with the goal of reducing redundant computations and improving overall recording performance.

Comment on lines +82 to +89
GVAR(excludeKindList) = parseSimpleArray EGVAR(settings,excludeKindFromRecord);
GVAR(excludeClassList) = parseSimpleArray EGVAR(settings,excludeClassFromRecord);

GVAR(excludeMarkerList) = if (!isNil QEGVAR(settings,excludeMarkerFromRecord)) then {
parseSimpleArray EGVAR(settings,excludeMarkerFromRecord)
} else {
[]
};

Choose a reason for hiding this comment

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

high

Caching the exclusion lists (excludeKindList, excludeClassList, excludeMarkerList) at initialization is a significant performance improvement. This prevents repeated parsing of these settings in performance-critical loops, such as the captureLoop and marker handlers. This aligns with the general rule of caching expensive operations.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

Comment on lines +44 to 70
private _localUnits = 0; private _localAlive = 0; private _remoteUnits = 0; private _remoteAlive = 0;
{ if (side _x isEqualTo _s) then {
if (local _x) then { _localUnits = _localUnits + 1; if (alive _x) then { _localAlive = _localAlive + 1 } }
else { _remoteUnits = _remoteUnits + 1; if (alive _x) then { _remoteAlive = _remoteAlive + 1 } };
}} forEach _allUnits;

private _localDead = 0; private _remoteDead = 0;
{ if (side _x isEqualTo _s) then {
if (local _x) then { _localDead = _localDead + 1 } else { _remoteDead = _remoteDead + 1 };
}} forEach _allDeadMen;

private _localGroups = 0; private _remoteGroups = 0;
{ if (side _x isEqualTo _s) then {
if (local _x) then { _localGroups = _localGroups + 1 } else { _remoteGroups = _remoteGroups + 1 };
}} forEach _allGroups;

private _localVeh = 0; private _remoteVeh = 0; private _localWH = 0; private _remoteWH = 0;
{ if (side _x isEqualTo _s) then {
private _isWH = _x isKindOf "WeaponHolderSimulated";
if (local _x) then { if (_isWH) then { _localWH = _localWH + 1 } else { _localVeh = _localVeh + 1 } }
else { if (_isWH) then { _remoteWH = _remoteWH + 1 } else { _remoteVeh = _remoteVeh + 1 } };
}} forEach _vehicles;

_sideData pushBack [
[count _localUnits, {alive _x} count _localUnits, count _localDead, count _localGroups, count _localVeh, count _localWH],
[count _remoteUnits, {alive _x} count _remoteUnits, count _remoteDead, count _remoteGroups, count _remoteVeh, count _remoteWH]
[_localUnits, _localAlive, _localDead, _localGroups, _localVeh, _localWH],
[_remoteUnits, _remoteAlive, _remoteDead, _remoteGroups, _remoteVeh, _remoteWH]
];

Choose a reason for hiding this comment

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

high

The refactoring of the telemetry loop to use single-pass counting instead of chained select calls is a significant performance improvement. The original code iterated over _allUnits, _allDeadMen, _allGroups, and _vehicles multiple times for each side. The new approach iterates once per collection and categorizes/counts within that single pass, drastically reducing redundant iterations. This directly addresses the general rule about avoiding multiple iterations for mutually exclusive categories.

References
  1. To optimize performance when counting items in an array that fall into two mutually exclusive categories, count one category directly and derive the count of the second by subtracting from the total. Avoid iterating over the array twice.

Comment on lines +180 to 184
if (_x getVariable [QGVARMAIN(unitData), []] isNotEqualTo _unitData) then {
_x setVariable [QGVARMAIN(unitData), +_unitData];
_unitData set [8, GVAR(captureFrameNo)];
[":SOLDIER:STATE:", _unitData] call EFUNC(extension,sendData);
_x setVariable [QGVARMAIN(unitData), _unitData];
};

Choose a reason for hiding this comment

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

high

The fix for unitData state deduplication is critical. By comparing against QGVARMAIN(unitData) and correctly setting the captureFrameNo before sending, redundant extension calls for dead/stationary units are eliminated. This directly addresses a high-impact performance issue.

Comment on lines +190 to +194
{
if ((str _marker) find _x >= 0) exitWith {
_isExcluded = true;
};
} forEach GVAR(excludeMarkerList);

Choose a reason for hiding this comment

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

medium

By using the pre-cached GVAR(excludeMarkerList), the expensive parseSimpleArray call is removed from this frequently executed loop, leading to improved performance.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

};
private _toExcludeClass = false;
private _classList = parseSimpleArray EGVAR(settings,excludeClassFromRecord);
private _classList = GVAR(excludeClassList);

Choose a reason for hiding this comment

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

medium

Similar to the excludeKindList, caching EGVAR(settings,excludeClassFromRecord) into GVAR(excludeClassList) and using the cached variable here avoids repeated parsing, leading to better performance in this per-frame handler.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

Comment on lines +274 to +278
{
if ((str _marker) find _x > -1) exitWith {
_isExcluded = true;
};
} forEach GVAR(excludeMarkerList);

Choose a reason for hiding this comment

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

medium

The change to use GVAR(excludeMarkerList) here is consistent with the other marker handlers and provides the same performance benefits by avoiding repeated parseSimpleArray calls.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

Comment on lines +297 to +301
{
if ((_marker) find _x >= 0) exitWith {
_isExcluded = true;
};
} forEach GVAR(excludeMarkerList);

Choose a reason for hiding this comment

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

medium

This change ensures that the initial marker collection also benefits from the cached exclusion list, preventing unnecessary parsing during mission initialization.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

private _vic = _x;
private _toExcludeKind = false;
private _kindList = parseSimpleArray EGVAR(settings,excludeKindFromRecord);
private _kindList = GVAR(excludeKindList);

Choose a reason for hiding this comment

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

medium

Replacing parseSimpleArray EGVAR(settings,excludeKindFromRecord) with the cached GVAR(excludeKindList) significantly improves performance by avoiding repeated parsing of the exclusion list within the per-frame handler. This aligns with the general rule of caching expensive operations in performance-critical code.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

Comment on lines +26 to +28
["Wheeled_APC_F","Tracked_APC","APC_Wheeled_01_base_F","APC_Wheeled_02_base_F",
"APC_Wheeled_03_base_F","APC_Tracked_01_base_F","APC_Tracked_02_base_F","APC_Tracked_03_base_F"]
findIf {_this isKindOf _x} != -1

Choose a reason for hiding this comment

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

medium

Replacing the count anti-pattern with findIf for early-exit in APC classification is a good optimization. count iterates through the entire array, while findIf stops as soon as a match is found, which is more efficient for this use case.

References
  1. In performance-critical code such as per-frame handlers, cache the results of expensive operations (e.g., parseSimpleArray) in local variables if the result is used multiple times, such as for both an emptiness check and a subsequent loop.

Comment on lines +152 to +158
private _scores = getPlayerScores _x;
private _scoresStr = _x getVariable [QGVAR(lastScoresStr), ""];
if (_scores isNotEqualTo (_x getVariable [QGVAR(lastScores), []])) then {
_scoresStr = _scores joinString ",";
_x setVariable [QGVAR(lastScores), _scores];
_x setVariable [QGVAR(lastScoresStr), _scoresStr];
};

Choose a reason for hiding this comment

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

medium

Caching getPlayerScores _x and its string representation (_scoresStr) prevents redundant calls to getPlayerScores and joinString if the scores haven't changed. This is a good performance improvement, especially in a per-frame handler.

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.

1 participant