Skip to content

perf(core): spread spray folder scans over timer ticks - #37

Open
Rushaway wants to merge 2 commits into
masterfrom
perf/budgeted-spray-scan
Open

Rushaway wants to merge 2 commits into
masterfrom
perf/budgeted-spray-scan

Conversation

@Rushaway

@Rushaway Rushaway commented Oct 6, 2026

Copy link
Copy Markdown
Member

Fixes #36

@cmer81 could you review this? It removes the 89 ms OnMapEnd and 57 ms TimerNext hitches by spreading the spray folder work over timer ticks.

What changed

  • OnMapEnd does no file work. It only starts a background scan.
  • One scan job, limited per tick. SprayScan replaces RecursiveFiles, RecursiveSearchDirs, TimerNext and DeleteEmptyDirs. It runs on a 0.1 s repeating timer (TimerScan) and stops after SCAN_BUDGET (2 ms) each tick, then continues where it left off. It runs in three steps:
    1. Walk the folders one entry at a time, keeping the directory listing open between ticks.
    2. Check (sm_spray_test) or move (map end, plugin start) the queued sprays.
    3. Remove emptied folders, deepest first. RemoveDir fails on folders that still hold files, so only empty ones go.
  • Plain filesystem calls. Scan file calls no longer use Valve FS, which searches every game path on each call. FileCheck already reads sprays this way.
  • No list shifting. Queued files are read by index instead of Erase(0).
  • Backup folder fix. The scan creates <root>/backup_sprays, the folder sprays are actually moved into. On CS:S, the old code created download/user_custom/backup_sprays instead.
  • sm_spray_test prints to the server console if the admin leaves before it finishes, instead of throwing an error.

OnPlayerRunCmd is unchanged. Outside impulse 202 it does one integer comparison, and removing it would reopen the jingle exploit.

Behaviour changes

  • If a move is still running when the next map ends, a new one isn't started. Files it misses are moved at the following map end.
  • A very large folder takes longer in wall-clock time. The old code also kept moving sprays into the next map, so this isn't new, but it can last longer. Raise SCAN_BUDGET to speed it up.

Testing

  • Builds with SourceMod 1.12.0.7255, no warnings
  • Change map: TimerScan stays around 2 ms and OnMapEnd drops out of the profiler
  • download/backup_sprays fills up and emptied user_custom/xx folders are removed
  • sm_spray_test finishes and prints its summary

🤖 Generated with Claude Code

OnMapEnd walked the whole download folder and moved the first 52
sprays in one call (89 ms), and each TimerNext tick moved 52 more
through Valve filesystem calls (57 ms).

- OnMapEnd now only starts the scan and does no file work
- One SprayScan job walks, checks or moves, then removes emptied
  folders, with at most SCAN_BUDGET (2 ms) of work per 0.1 s tick
- Scan file calls use the plain filesystem instead of Valve FS
- Read the queued files by index instead of Erase(0)
- Create the backup folder that sprays are actually moved into
- sm_spray_test falls back to the server console if the admin leaves

Fixes #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 19:21
@Rushaway Rushaway added the bug Something isn't working label Oct 6, 2026
@Rushaway
Rushaway requested a review from cmer81 October 6, 2026 19:21
@Rushaway Rushaway added the bug Something isn't working label Oct 6, 2026

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.

Copilot review overview

🟡 Changes recommended

Backup-directory creation still uses Valve FS, contrary to the performance objective.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Refactors spray-folder maintenance into incremental timer-based scans to reduce server-thread stalls.

Changes:

  • Adds budgeted asynchronous scanning, validation, moving, and cleanup.
  • Fixes backup-folder targeting and disconnected-admin output.
  • Updates Copilot guidance for the new scanning architecture.
File Description
FixSprayExploit.sp Implements incremental spray scanning and cleanup.
.github/​copilot-instructions.md Documents the updated workflow.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread addons/sourcemod/scripting/FixSprayExploit.sp Outdated
The moves rename into the backup folder through plain paths under the
game folder, so create it the same way instead of under Valve's write
path. The scan root is already open at that point, so the non-recursive
CreateDirectory is enough.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cmer81 cmer81 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed and built on SourceMod 1.13.0.7427 (base and head compile, no new warnings).

The incremental scan looks correct: directories are removed leaf-first (visited is filled parent-before-child and popped in reverse, RemoveDir refuses non-empty folders), the scan root is no longer a removal candidate, the active guard prevents overlapping scans and all handles are freed at the end, and a disconnected admin falls back to the server console.

Performance: OnMapEnd now only queues the scan, and each tick does at most ~2 ms of work with the overshoot bounded to a single file operation (the clock is checked before every operation). Replacing Erase(0) by an index also removes the O(n^2) cost on large queues. The trade-off is a throughput cap (~2% duty cycle), which is acceptable here. This is from reading the code, I have not profiled it on a live server.

One thing to fix before merging: the behaviour changes but PLUGIN_VERSION is still "2.28", please bump it (e.g. 2.29).

Minor, non-blocking: the final sm_spray_test message always goes to the console now (the original reply source is not kept), which is fine.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FixSprayExploit blocks the server for up to 89 ms (OnMapEnd, TimerNext)

3 participants