Skip to main content
Glama
Idle.ts17.5 kB
import { AdbUtils } from "../../utils/android-cmdline-tools/adb"; import { logger } from "../../utils/logger"; import { UiStabilityResult, TouchIdleResult, RotationCheckResult, BootedDevice } from "../../models"; export class Idle { private adb: AdbUtils; /** * Create an Idle instance * @param device - Optional device ID * @param adb */ constructor(device: BootedDevice, adb: AdbUtils | null = null) { this.adb = adb || new AdbUtils(device); } /** * Check if a package is a system/launcher package that might not provide meaningful gfxinfo * @param packageName - Package name to check * @returns True if this is likely a system package */ private isSystemLauncher(packageName: string | null): boolean { // Handle null, undefined, or empty package names if (!packageName) { return false; } const systemPackages = [ "com.android.systemui", "com.android.launcher", "com.android.launcher3", "com.google.android.apps.nexuslauncher", "com.samsung.android.app.launcher", "com.miui.home", "com.oneplus.launcher", "com.huawei.android.launcher", "com.sec.android.app.launcher", "com.android.settings" ]; return systemPackages.some(sysPackage => packageName.includes(sysPackage) || sysPackage.includes(packageName) ); } /** * Check touch idle status * @param startTime - When the idle checking started * @param lastEventTime - When the last touch event was detected * @param timeoutMs - Required idle timeout in milliseconds * @param hardLimitMs - Hard timeout limit in milliseconds * @returns Object containing idle check results */ getTouchStatus( startTime: number, lastEventTime: number, timeoutMs: number, hardLimitMs: number ): TouchIdleResult { const currentElapsed = Date.now() - startTime; const idleTime = Date.now() - lastEventTime; const isIdle = idleTime >= timeoutMs; const shouldContinue = !isIdle && currentElapsed < hardLimitMs; return { isIdle, shouldContinue, currentElapsed, idleTime }; } /** * Check rotation status against target * @param targetRotation - The expected rotation value * @param startTime - When rotation checking started * @param timeoutMs - Maximum time to wait for rotation * @returns Object containing rotation check results */ async getRotationStatus( targetRotation: number, startTime: number, timeoutMs: number ): Promise<RotationCheckResult> { const currentElapsed = Date.now() - startTime; const shouldContinue = currentElapsed < timeoutMs; try { // Check the current rotation through window manager service const { stdout } = await this.adb.executeCommand('shell dumpsys window | grep -i "mRotation="'); const rotationMatch = stdout.match(/mRotation=(\d+)/); if (rotationMatch) { const currentRotation = parseInt(rotationMatch[1], 10); logger.debug(`Current rotation: ${currentRotation}, target: ${targetRotation}`); if (currentRotation === targetRotation) { logger.debug(`Rotation to ${targetRotation} complete, took ${currentElapsed}ms`); return { rotationComplete: true, currentRotation, shouldContinue: false }; } return { rotationComplete: false, currentRotation, shouldContinue }; } return { rotationComplete: false, currentRotation: null, shouldContinue }; } catch (err) { // Just continue polling on error return { rotationComplete: false, currentRotation: null, shouldContinue }; } } /** * Measure UI idle status by resetting gfx stats and taking an immediate measurement * @param packageName - Package name of the app to monitor * @param measurementDelayMs - Time to wait after reset before measuring (default 200ms) * @returns Object containing UI idle measurement results */ async getUiStabilitySnapshot( packageName: string, measurementDelayMs: number = 200 ): Promise<UiStabilityResult> { logger.info(`[AwaitIdle] Measuring UI idle for ${packageName} with ${measurementDelayMs}ms delay`); try { // Reset the gfxinfo stats for the package await this.adb.executeCommand(`shell dumpsys gfxinfo ${packageName} reset`); // Wait for measurement period to accumulate data await new Promise(resolve => setTimeout(resolve, measurementDelayMs)); // Take immediate measurement with no previous state return await this.getUiStability( packageName, null, // No previous missed vsync null, // No previous slow UI thread null, // No previous frame deadline missed false // Not first log since we just reset ); } catch (err) { logger.info(`[Idle] Error measuring UI idle: ${err}`); return { isStable: false, shouldUpdateLastNonIdleTime: true, updatedPrevMissedVsync: null, updatedPrevSlowUiThread: null, updatedPrevFrameDeadlineMissed: null, updatedFirstGfxInfoLog: false }; } } /** * Get frame stats from adb command * @param packageName - Package name of the app * @param firstGfxInfoLog - Whether this is the first gfx info log * @returns Frame stats output string */ private async getFrameStats(packageName: string, firstGfxInfoLog: boolean): Promise<string> { try { const { stdout } = await this.adb.executeCommand(`shell dumpsys gfxinfo ${packageName}`); if (firstGfxInfoLog) { logger.info(`[AwaitIdle] Initial gfxinfo stdout for ${packageName}:\n${stdout}`); } return stdout; } catch (error) { // If gfxinfo fails, return empty string to trigger fallback behavior logger.info(`[AwaitIdle] Failed to get gfxinfo for ${packageName}: ${error}`); return ""; } } /** * Parse all metrics from gfxinfo output * @param stdout - The gfxinfo output string * @returns Object containing parsed metrics */ parseMetrics(stdout: string): { percentile50th: number | null; percentile90th: number | null; percentile95th: number | null; percentile99th: number | null; missedVsync: number | null; slowUiThread: number | null; frameDeadlineMissed: number | null; } { const percentile50th = this.extractMetric(stdout, /50th percentile:\s+(\d+(?:\.\d+)?)ms/); const percentile90th = this.extractMetric(stdout, /90th percentile:\s+(\d+(?:\.\d+)?)ms/); const percentile95th = this.extractMetric(stdout, /95th percentile:\s+(\d+(?:\.\d+)?)ms/); const percentile99th = this.extractMetric(stdout, /99th percentile:\s+(\d+(?:\.\d+)?)ms/); const missedVsync = this.extractMetric(stdout, /Number Missed Vsync:\s+(\d+)/); const slowUiThread = this.extractMetric(stdout, /Number Slow UI thread:\s+(\d+)/); const frameDeadlineMissed = this.extractMetric(stdout, /Number Frame deadline missed:\s+(\d+)/); logger.debug(`Metrics: 50th=${percentile50th}ms 90th=${percentile90th}ms 95th=${percentile95th}ms 99th=${percentile99th}ms MissedVsync=${missedVsync} SlowUI=${slowUiThread} DeadlineMissed=${frameDeadlineMissed}`); return { percentile50th, percentile90th, percentile95th, percentile99th, missedVsync, slowUiThread, frameDeadlineMissed }; } /** * Validate frame data metrics * @param metrics - Parsed metrics object * @returns True if metrics contain valid frame data */ public validateFrameData(metrics: { percentile50th: number | null; missedVsync: number | null; slowUiThread: number | null; }): boolean { return metrics.percentile50th !== null && metrics.missedVsync !== null && metrics.slowUiThread !== null; } /** * Update stability state with current metrics * @param current - Current metric values * @param previous - Previous metric values * @returns Updated state object */ public updateStabilityState( current: { missedVsync: number | null; slowUiThread: number | null; frameDeadlineMissed: number | null }, previous: { missedVsync: number | null; slowUiThread: number | null; frameDeadlineMissed: number | null } ): { updatedPrevMissedVsync: number | null; updatedPrevSlowUiThread: number | null; updatedPrevFrameDeadlineMissed: number | null; deltas: { missedVsyncDelta: number; slowUiThreadDelta: number; frameDeadlineMissedDelta: number }; } { const updatedPrevMissedVsync = current.missedVsync; const updatedPrevSlowUiThread = current.slowUiThread; const updatedPrevFrameDeadlineMissed = current.frameDeadlineMissed; const deltas = this.calculateDeltas(current, previous); return { updatedPrevMissedVsync, updatedPrevSlowUiThread, updatedPrevFrameDeadlineMissed, deltas }; } /** * Process frame stats and determine stability * @param stdout - Frame stats output * @param prevMissedVsync - Previous missed vsync count * @param prevSlowUiThread - Previous slow UI thread count * @param prevFrameDeadlineMissed - Previous frame deadline missed count * @returns Object containing stability determination and updated state */ public processFrameStatsForStability( stdout: string, prevMissedVsync: number | null, prevSlowUiThread: number | null, prevFrameDeadlineMissed: number | null ): { isStable: boolean; shouldUpdateLastNonIdleTime: boolean; updatedPrevMissedVsync: number | null; updatedPrevSlowUiThread: number | null; updatedPrevFrameDeadlineMissed: number | null; } { // Parse specific metrics const metrics = this.parseMetrics(stdout); // Check if we have valid data if (!this.validateFrameData(metrics)) { logger.info(`[AwaitIdle] No valid frame data yet: percentile50th ${metrics.percentile50th} && missedVsync ${metrics.missedVsync} && slowUiThread ${metrics.slowUiThread}`); return { isStable: false, shouldUpdateLastNonIdleTime: true, updatedPrevMissedVsync: prevMissedVsync, updatedPrevSlowUiThread: prevSlowUiThread, updatedPrevFrameDeadlineMissed: prevFrameDeadlineMissed }; } // Update state with current values and calculate deltas const stateUpdate = this.updateStabilityState( { missedVsync: metrics.missedVsync, slowUiThread: metrics.slowUiThread, frameDeadlineMissed: metrics.frameDeadlineMissed }, { missedVsync: prevMissedVsync, slowUiThread: prevSlowUiThread, frameDeadlineMissed: prevFrameDeadlineMissed } ); // Check stability criteria const isStable = this.checkStabilityCriteria(stateUpdate.deltas, { percentile50th: metrics.percentile50th, percentile90th: metrics.percentile90th, percentile95th: metrics.percentile95th }); return { isStable, shouldUpdateLastNonIdleTime: !isStable, updatedPrevMissedVsync: stateUpdate.updatedPrevMissedVsync, updatedPrevSlowUiThread: stateUpdate.updatedPrevSlowUiThread, updatedPrevFrameDeadlineMissed: stateUpdate.updatedPrevFrameDeadlineMissed }; } /** * Calculate deltas between current and previous metrics * @param current - Current metric values * @param previous - Previous metric values * @returns Object containing calculated deltas */ calculateDeltas( current: { missedVsync: number | null; slowUiThread: number | null; frameDeadlineMissed: number | null }, previous: { missedVsync: number | null; slowUiThread: number | null; frameDeadlineMissed: number | null } ): { missedVsyncDelta: number; slowUiThreadDelta: number; frameDeadlineMissedDelta: number; } { const missedVsyncDelta = previous.missedVsync !== null && current.missedVsync !== null ? current.missedVsync - previous.missedVsync : 0; const slowUiThreadDelta = previous.slowUiThread !== null && current.slowUiThread !== null ? current.slowUiThread - previous.slowUiThread : 0; const frameDeadlineMissedDelta = previous.frameDeadlineMissed !== null && current.frameDeadlineMissed !== null ? current.frameDeadlineMissed - previous.frameDeadlineMissed : 0; logger.debug(`Deltas: MissedVsync=${missedVsyncDelta} SlowUI=${slowUiThreadDelta} DeadlineMissed=${frameDeadlineMissedDelta}`); return { missedVsyncDelta, slowUiThreadDelta, frameDeadlineMissedDelta }; } /** * Check if metrics meet stability criteria * @param deltas - Delta values between measurements * @param percentiles - Current percentile metrics * @returns Whether the UI is stable */ checkStabilityCriteria( deltas: { missedVsyncDelta: number; slowUiThreadDelta: number; frameDeadlineMissedDelta: number }, percentiles: { percentile50th: number | null; percentile90th: number | null; percentile95th: number | null } ): boolean { // Check idle criteria: // - Zero delta in missed vsyncs // - Zero delta in slow UI threads // - Zero delta in frame deadline missed // - All percentiles < reasonable thresholds const p50Int = percentiles.percentile50th !== null ? Math.floor(percentiles.percentile50th) : 0; const p90Int = percentiles.percentile90th !== null ? Math.floor(percentiles.percentile90th) : 0; const p95Int = percentiles.percentile95th !== null ? Math.floor(percentiles.percentile95th) : 0; const isStable = deltas.missedVsyncDelta === 0 && deltas.slowUiThreadDelta === 0 && deltas.frameDeadlineMissedDelta === 0 && p50Int < 100 && p90Int < 100 && p95Int < 200; if (isStable) { logger.info("[AwaitIdle] UI appears stable (criteria met)"); } else { logger.info(`[AwaitIdle] UI not stable: deltas (Vsync=${deltas.missedVsyncDelta}, UI=${deltas.slowUiThreadDelta}, Deadline=${deltas.frameDeadlineMissedDelta}), percentiles (50th=${p50Int}, 90th=${p90Int}, 95th=${p95Int})`); } return isStable; } /** * Check UI stability metrics and return calculation results * @param packageName - Package name of the app to monitor * @param prevMissedVsync - Previous missed vsync count * @param prevSlowUiThread - Previous slow UI thread count * @param prevFrameDeadlineMissed - Previous frame deadline missed count * @param firstGfxInfoLog - Whether this is the first gfx info log * @returns Object containing stability check results and updated state */ async getUiStability( packageName: string, prevMissedVsync: number | null, prevSlowUiThread: number | null, prevFrameDeadlineMissed: number | null, firstGfxInfoLog: boolean ): Promise<UiStabilityResult> { try { // For system packages, use a simpler approach if (this.isSystemLauncher(packageName)) { logger.info(`[AwaitIdle] ${packageName} is a system package, using simplified stability check`); return { isStable: true, shouldUpdateLastNonIdleTime: false, updatedPrevMissedVsync: null, updatedPrevSlowUiThread: null, updatedPrevFrameDeadlineMissed: null, updatedFirstGfxInfoLog: false }; } // Get the frame stats const stdout = await this.getFrameStats(packageName, firstGfxInfoLog); // If we get empty output, treat as stable (package might not support gfxinfo) if (!stdout || stdout.trim() === "") { logger.info(`[AwaitIdle] No gfxinfo data for ${packageName}, treating as stable`); return { isStable: true, shouldUpdateLastNonIdleTime: false, updatedPrevMissedVsync: null, updatedPrevSlowUiThread: null, updatedPrevFrameDeadlineMissed: null, updatedFirstGfxInfoLog: false }; } // Process frame stats and determine stability const result = this.processFrameStatsForStability( stdout, prevMissedVsync, prevSlowUiThread, prevFrameDeadlineMissed ); return { isStable: result.isStable, shouldUpdateLastNonIdleTime: result.shouldUpdateLastNonIdleTime, updatedPrevMissedVsync: result.updatedPrevMissedVsync, updatedPrevSlowUiThread: result.updatedPrevSlowUiThread, updatedPrevFrameDeadlineMissed: result.updatedPrevFrameDeadlineMissed, updatedFirstGfxInfoLog: false }; } catch (err) { // Just continue polling on error logger.info(`[AwaitIdle] Error checking frame stats: ${err}`); return { isStable: false, shouldUpdateLastNonIdleTime: true, updatedPrevMissedVsync: prevMissedVsync, updatedPrevSlowUiThread: prevSlowUiThread, updatedPrevFrameDeadlineMissed: prevFrameDeadlineMissed, updatedFirstGfxInfoLog: false }; } } /** * Helper method to extract numeric metrics from gfxinfo output * @param output - The gfxinfo output string * @param regex - Regular expression to match the metric * @returns The extracted number or null if not found */ extractMetric(output: string, regex: RegExp): number | null { const match = output.match(regex); if (match && match[1]) { const value = parseFloat(match[1]); return isNaN(value) ? null : value; } return null; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zillow/auto-mobile'

If you have feedback or need assistance with the MCP directory API, please join our Discord server