Prevent iOS daemon throttling with SSH launch and priority boosts

iOS aggressively throttles daemon processes, causing CADisplayLink callbacks
to be delayed or skipped. This commit implements multiple workarounds:

- Replace CADisplayLink with high-priority dispatch timer (QOS_CLASS_USER_INTERACTIVE)
- Add power assertions to prevent display/GPU/CPU throttling
- Launch daemon via SSH to localhost for better scheduler treatment
- Set real-time thread priority and ProcessType=Interactive
- Add FPS diagnostics logging to monitor capture performance
- Auto-setup passwordless SSH keys during package installation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mousen
2025-12-08 08:12:48 +05:00
parent aecc88aed2
commit 6d5d143dd9
6 changed files with 194 additions and 20 deletions

View File

@@ -1,4 +1,5 @@
#import "FrameUpdater.h"
#import <mach/mach_time.h>
@implementation FrameUpdater {
@@ -6,6 +7,7 @@
NSOperationQueue *_q;
BOOL _updatingFrames;
CADisplayLink *_displayLink;
dispatch_source_t _timer;
// Shared from ScreenDumpVNC
IOSurfaceRef _screenSurface;
@@ -20,6 +22,14 @@
// Dual capture mode support
IOSurfaceRef _renderServerSurface; // Separate surface for CARenderServer (avoids framebuffer locking)
CaptureMode(^_captureModeBlock)(void);
// Diagnostics
uint64_t _timerFires;
uint64_t _framesRendered;
uint64_t _framesSkipped;
uint64_t _lastStatsTime;
uint64_t _totalCaptureTime;
uint64_t _maxCaptureTime;
}
-(instancetype)initWithSurfaceInfo:(IOSurfaceRef)screenSurface
@@ -35,6 +45,7 @@
if ((self = [super init])) {
_q = [[NSOperationQueue alloc] init];
_q.maxConcurrentOperationCount = 1; // Serialize to prevent frame queue buildup
_q.qualityOfService = NSQualityOfServiceUserInteractive;
_updatingFrames = NO;
_displayLink = nil;
@@ -60,12 +71,19 @@
return;
}
_timerFires++;
// Skip if queue is busy to prevent frame buildup and reduce lock contention
if (_q.operationCount > 0) {
_framesSkipped++;
return;
}
[_q addOperationWithBlock: ^{
_framesRendered++;
uint64_t captureStart = mach_absolute_time();
// Get current capture mode
CaptureMode mode = _captureModeBlock ? _captureModeBlock() : CaptureModeCARenderServer;
BOOL needsScaling = (_width != _nativeWidth || _height != _nativeHeight);
@@ -86,28 +104,78 @@
}
}
uint64_t captureEnd = mach_absolute_time();
uint64_t captureDuration = captureEnd - captureStart;
_totalCaptureTime += captureDuration;
if (captureDuration > _maxCaptureTime) _maxCaptureTime = captureDuration;
rfbMarkRectAsModified(_rfbScreenInfo, 0, 0, _width, _height);
// Log stats every second
uint64_t now = mach_absolute_time();
if (_lastStatsTime == 0) _lastStatsTime = now;
mach_timebase_info_data_t timebase;
mach_timebase_info(&timebase);
uint64_t elapsed_ns = (now - _lastStatsTime) * timebase.numer / timebase.denom;
if (elapsed_ns >= NSEC_PER_SEC) {
double elapsed_sec = (double)elapsed_ns / NSEC_PER_SEC;
double avgCaptureMs = (_framesRendered > 0) ?
((double)_totalCaptureTime * timebase.numer / timebase.denom / _framesRendered / 1000000.0) : 0;
double maxCaptureMs = (double)_maxCaptureTime * timebase.numer / timebase.denom / 1000000.0;
NSLog(@"[FPS] fps=%.1f skip=%.0f%% | capture: avg=%.1fms max=%.1fms",
_framesRendered / elapsed_sec,
(_timerFires > 0) ? (100.0 * _framesSkipped / _timerFires) : 0.0,
avgCaptureMs, maxCaptureMs);
_timerFires = 0;
_framesRendered = 0;
_framesSkipped = 0;
_totalCaptureTime = 0;
_maxCaptureTime = 0;
_lastStatsTime = now;
}
}];
}
-(void)stopFrameLoop {
if (_displayLink == nil) return;
_updatingFrames = NO;
if (_timer) {
dispatch_source_cancel(_timer);
_timer = nil;
}
if (_displayLink) {
dispatch_async(dispatch_get_main_queue(), ^(void){
[_displayLink invalidate];
_displayLink = nil;
_updatingFrames = NO;
});
}
}
-(void)startFrameLoop {
[self stopFrameLoop];
_updatingFrames = YES;
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_updateFrame)];
displayLink.preferredFramesPerSecond = 60; // Limit to 60 FPS
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
_displayLink = displayLink;
// Use high-priority dispatch timer instead of CADisplayLink
// CADisplayLink may be throttled for daemon processes
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
dispatch_queue_t timerQueue = dispatch_queue_create("com.mousen.screendump.frametimer", attr);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, timerQueue);
// 60 FPS = ~16.67ms interval
uint64_t interval = NSEC_PER_SEC / 60;
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
dispatch_source_set_event_handler(_timer, ^{
[self _updateFrame];
});
dispatch_resume(_timer);
}
-(void)dealloc {

View File

@@ -2,19 +2,20 @@ export THEOS_PACKAGE_SCHEME = rootless
export ARCHS = arm64
export TARGET = iphone:16.5:14.0
export GO_EASY_ON_ME = 1
export COPYFILE_DISABLE=1
export COPYFILE_DISABLE = 1
include $(THEOS)/makefiles/common.mk
TOOL_NAME = screendumpd
$(TOOL_NAME)_FILES = $(wildcard *.m)
$(TOOL_NAME)_FRAMEWORKS := IOSurface IOKit
$(TOOL_NAME)_PRIVATE_FRAMEWORKS := IOMobileFramebuffer IOSurface
$(TOOL_NAME)_OBJCFLAGS += -I./vncbuild/include -Iinclude -fobjc-arc
$(TOOL_NAME)_LDFLAGS += -Wl,-segalign,4000 -L./vncbuild/lib -lvncserver -lpng -llzo2 -ljpeg -lssl -lcrypto -lz
$(TOOL_NAME)_CFLAGS = -w
$(TOOL_NAME)_CODESIGN_FLAGS = "-Sen.plist"
$(TOOL_NAME)_INSTALL_PATH = /usr/libexec
screendumpd_FILES = $(wildcard *.m)
screendumpd_FRAMEWORKS = IOSurface IOKit
screendumpd_PRIVATE_FRAMEWORKS = IOMobileFramebuffer IOSurface
screendumpd_OBJCFLAGS = -I./vncbuild/include -Iinclude -fobjc-arc
screendumpd_LDFLAGS = -Wl,-segalign,4000 -L./vncbuild/lib -lvncserver -lpng -llzo2 -ljpeg -lssl -lcrypto -lz
screendumpd_CFLAGS = -w
screendumpd_CODESIGN_FLAGS = -Sen.plist
screendumpd_INSTALL_PATH = /usr/libexec
before-stage::
$(ECHO_NOTHING)find . -name '.DS_Store' -type f -delete$(ECHO_END)

View File

@@ -5,6 +5,7 @@
#import <UIKit/UIKit.h>
#import <rfb/rfb.h>
#import "IOMobileFramebuffer.h"
#import <IOKit/pwr_mgt/IOPMLib.h>
@implementation ScreenDumpVNC {
int _prefsHeight;
@@ -38,6 +39,25 @@
+(void)load {
ScreenDumpVNC* sharedInstance = [self sharedInstance];
if (![sharedInstance enabled]) return;
// Tell iOS this is latency-critical work - prevents throttling for daemons
[[NSProcessInfo processInfo] beginActivityWithOptions:(NSActivityLatencyCritical | NSActivityUserInitiated)
reason:@"VNC screen streaming"];
// Power assertion to prevent display/GPU throttling
IOPMAssertionID assertionID;
IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep,
kIOPMAssertionLevelOn,
CFSTR("VNC screen capture requires display access"),
&assertionID);
// Also assert we need system activity (prevents CPU throttling)
IOPMAssertionID systemAssertionID;
IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleSystemSleep,
kIOPMAssertionLevelOn,
CFSTR("VNC server active"),
&systemAssertionID);
[sharedInstance setupScreenInfo];
[sharedInstance startVNCServer];
}

View File

@@ -1,3 +1,40 @@
#!/bin/sh
# Setup passwordless SSH for root to localhost (required for screendumpd performance)
SSH_DIR="/var/jb/var/root/.ssh"
KEY_FILE="$SSH_DIR/id_ed25519"
AUTH_KEYS="$SSH_DIR/authorized_keys"
# Create .ssh directory if it doesn't exist
if [ ! -d "$SSH_DIR" ]; then
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
fi
# Generate ed25519 key if it doesn't exist
if [ ! -f "$KEY_FILE" ]; then
ssh-keygen -t ed25519 -N "" -f "$KEY_FILE" -q
chmod 600 "$KEY_FILE"
chmod 644 "${KEY_FILE}.pub"
fi
# Add public key to authorized_keys if not already present
if [ -f "${KEY_FILE}.pub" ]; then
PUB_KEY=$(cat "${KEY_FILE}.pub")
# Create authorized_keys if it doesn't exist
if [ ! -f "$AUTH_KEYS" ]; then
touch "$AUTH_KEYS"
chmod 600 "$AUTH_KEYS"
fi
# Check if key is already in authorized_keys
if ! grep -qF "$PUB_KEY" "$AUTH_KEYS" 2>/dev/null; then
echo "$PUB_KEY" >> "$AUTH_KEYS"
fi
fi
# Load the launch daemon
launchctl load /var/jb/Library/LaunchDaemons/com.mousen.screendumpd.plist 2> /dev/null
exit 0;
exit 0

View File

@@ -6,6 +6,14 @@
<string>com.mousen.screendumpd</string>
<key>ProgramArguments</key>
<array>
<string>/var/jb/usr/bin/ssh</string>
<string>-o</string>
<string>StrictHostKeyChecking=no</string>
<string>-o</string>
<string>UserKnownHostsFile=/dev/null</string>
<string>-o</string>
<string>BatchMode=yes</string>
<string>root@localhost</string>
<string>/var/jb/usr/libexec/screendumpd</string>
</array>
<key>RunAtLoad</key>
@@ -22,5 +30,13 @@
<key>Core</key>
<integer>9223372036854775807</integer>
</dict>
<key>ProcessType</key>
<string>Interactive</string>
<key>Nice</key>
<integer>-10</integer>
<key>LegacyTimers</key>
<true/>
<key>UserName</key>
<string>root</string>
</dict>
</plist>

View File

@@ -1,12 +1,44 @@
#import <Foundation/Foundation.h>
#import <stdio.h>
#import <signal.h>
#import <unistd.h>
#import <pthread.h>
#import <sched.h>
#import <dispatch/dispatch.h>
#import "ScreenDumpVNC.h"
#import "utils.h"
#define kPreferencesNotify "com.mousen.screendump/restart"
static void signalHandler(int sig) {
exit(0);
}
int main(int argc, char *argv[], char *envp[]) {
@autoreleasepool {
// Handle termination signals (sent by launchd or when SSH disconnects)
signal(SIGTERM, signalHandler);
signal(SIGHUP, signalHandler);
signal(SIGINT, signalHandler);
// Monitor parent process - exit if parent (sshd) dies (ppid becomes 1)
pid_t originalPpid = getppid();
dispatch_source_t parentTimer = dispatch_source_create(
DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(parentTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(parentTimer, ^{
if (getppid() != originalPpid) {
// Parent changed (likely died and we got reparented to launchd/init)
exit(0);
}
});
dispatch_resume(parentTimer);
// Boost main thread to real-time priority for better daemon performance
struct sched_param param;
param.sched_priority = sched_get_priority_max(SCHED_RR);
pthread_setschedparam(pthread_self(), SCHED_RR, &param);
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)exitProcess, CFSTR(kPreferencesNotify), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
[ScreenDumpVNC load];