๐ŸŽฏ frame_scheduler

A production-grade, FPS-aware task scheduler for Flutter apps and games.

pub.flutter-io.cn License: MIT Flutter Dart Platform


๐Ÿง  The Problem

Your Flutter app runs at 60 FPS in normal conditions. Then it needs to execute a heavy task โ€” parsing a large JSON response, pre-loading the next scene's assets, processing a list of 5,000 items. The FPS tanks to 15, the animation freezes, and the user feels the jank.

The problem isn't the work itself โ€” it's the timing.

๐Ÿ’ก The Solution

frame_scheduler hooks into Flutter's rendering engine via SchedulerBinding.addTimingsCallback, monitors the real-time FPS continuously, and automatically defers, prioritises, or drops tasks based on the current performance health:

FPS is healthy?  โ†’ Execute now
FPS is degraded? โ†’ Defer task to the priority queue
FPS recovered?   โ†’ Execute deferred tasks within the frame budget
FPS in danger?   โ†’ Drop non-critical tasks to relieve UI pressure

No polling loops. No manual FPS checks. No Timer.periodic hacks. Just schedule โ€” the library decides when.


โœจ Features

Feature Description
๐Ÿ”ฌ Real-time FPS monitoring Uses SchedulerBinding.addTimingsCallback โ€” the deepest, most accurate Flutter FPS source
๐ŸŽš 4-level priority system Critical / High / Normal / Low with automatic deferral rules
โฑ Frame-budget awareness Never schedules more work than fits in the available frame time
๐Ÿ”„ Smart deferred queue Priority-sorted, bounded, deduplicated queue with expiry support
๐Ÿ“Š Built-in metrics Executed / Deferred / Dropped / Expired counters with drop rate
๐Ÿ”ง 4 presets + copyWith balanced, performance, batterySaver, highRefresh
๐Ÿ— Auto priority escalation Tasks close to expiry are automatically upgraded
๐ŸŽจ Debug overlay FpsOverlay shows live FPS, zone, and queue depth
๐Ÿ“ก Reactive widget SchedulerBuilder rebuilds on zone changes
๐Ÿ”Œ Zero dependencies Pure Flutter + Dart, no third-party packages

๐Ÿ“ฆ Installation

# pubspec.yaml
dependencies:
  frame_scheduler: ^1.0.0
flutter pub get

๐Ÿš€ Quick Start โ€” 3 Steps

Step 1 โ€” Wrap your app

import 'package:frame_scheduler/frame_scheduler.dart';

void main() {
  runApp(
    FrameSchedulerScope(
      config: SchedulerConfig.performance(),
      child: const MyApp(),
    ),
  );
}

Step 2 โ€” Schedule your tasks

final scheduler = SchedulerController.instance;

// Load assets for the next scene (deferrable)
scheduler.schedule(
  () async => await assetLoader.preload('level_3'),
  priority: PriorityLevel.high,
  estimatedDurationMs: 80.0,
  id: 'preload_level_3',
  maxWaitMs: 5000,
  onDropped: () => print('Preload dropped โ€” will use fallback'),
);

// Send analytics (safe to drop)
scheduler.schedule(
  () async => await analytics.flush(),
  priority: PriorityLevel.low,
  estimatedDurationMs: 2.0,
  maxWaitMs: 10000,
  onDropped: () => print('Analytics dropped โ€” no FPS budget'),
);

// Critical โ€” always executes instantly
scheduler.schedule(
  () async => soundEngine.play('coin'),
  priority: PriorityLevel.critical,
);

Step 3 โ€” Add the debug overlay (development only)

Stack(
  children: [
    const MyGameScreen(),
    if (kDebugMode) const FpsOverlay(showMetrics: true),
  ],
)

๐Ÿ“Š FPS Zones

Zone Range (60fps target) Emoji Behaviour
Healthy โ‰ฅ 48 FPS ๐ŸŸข All tasks execute immediately
Warning 30โ€“48 FPS ๐ŸŸก Normal + Low priority tasks deferred
Critical 15โ€“30 FPS ๐Ÿ”ด Only Critical + High execute; others drop
Danger < 15 FPS ๐Ÿ’€ Only Critical executes; queue is purged

๐ŸŽš Priority Behaviour Matrix

Priority ๐ŸŸข Healthy ๐ŸŸก Warning ๐Ÿ”ด Critical ๐Ÿ’€ Danger
critical Execute Execute Execute Execute
high Execute Execute Defer Drop
normal Execute Defer Drop Drop
low Execute Defer Drop Drop

โš™๏ธ Configuration Presets

SchedulerConfig.balanced() โ€” Default

// Best for: General apps, content browsing, social media
FrameSchedulerScope(config: SchedulerConfig.balanced())
Parameter Value
Target FPS 60
Warning threshold 48 FPS (80%)
Critical threshold 30 FPS (50%)
Window size 60 frames
Max queue 50 tasks

SchedulerConfig.performance() โ€” Aggressive

// Best for: Heavy games, 3D apps, real-time simulations
FrameSchedulerScope(config: SchedulerConfig.performance())
Parameter Value
Target FPS 60
Warning threshold 54 FPS (90%) โ€” defers sooner
Critical threshold 40 FPS
Window size 30 frames โ€” reacts faster
Max queue 100 tasks

SchedulerConfig.batterySaver() โ€” Low-end Devices

// Best for: Budget phones, older devices, background processing
FrameSchedulerScope(config: SchedulerConfig.batterySaver())
Parameter Value
Target FPS 30
Warning threshold 24 FPS
Check interval 500ms โ€” fewer wake-ups

SchedulerConfig.highRefresh() โ€” 120Hz Displays

// Best for: iPad Pro, OnePlus, Samsung Galaxy S, Pixel 6+
FrameSchedulerScope(config: SchedulerConfig.highRefresh())
Parameter Value
Target FPS 120
Warning threshold 96 FPS (80%)
Critical threshold 60 FPS (50%)
Window size 120 frames

Custom Configuration

SchedulerConfig.balanced().copyWith(
  maxDeferredTasks: 200,
  deferCheckIntervalMs: 100,
  safeBudgetRatio: 0.60,
  autoAdjustPriority: true,
)

๐Ÿ— Architecture

frame_scheduler/
โ”‚
โ”œโ”€โ”€ FpsMonitor              โ† Reads FrameTiming from SchedulerBinding
โ”‚     โ””โ”€โ”€ rolling window   โ† Smoothed FPS via N-frame average
โ”‚
โ”œโ”€โ”€ FrameBudget             โ† 16.67ms / 120fps budget calculator
โ”‚     โ””โ”€โ”€ computeZone()    โ† Maps FPS โ†’ FpsZone (healthy/warning/critical/danger)
โ”‚
โ”œโ”€โ”€ TaskQueue               โ† Priority-sorted, bounded, deduplicated queue
โ”‚     โ”œโ”€โ”€ PriorityLevel     โ† critical / high / normal / low
โ”‚     โ””โ”€โ”€ ScheduledTask     โ† Task + metadata (id, estimated duration, expiry)
โ”‚
โ”œโ”€โ”€ SchedulerController     โ† Singleton orchestrator โ€” THE main API
โ”‚     โ”œโ”€โ”€ schedule()        โ† FPS-aware scheduling entry point
โ”‚     โ”œโ”€โ”€ runCritical()     โ† Bypass-all emergency execution
โ”‚     โ””โ”€โ”€ metrics           โ† Cumulative SchedulerMetrics snapshot
โ”‚
โ”œโ”€โ”€ SchedulerConfig         โ† Immutable config object (4 presets + copyWith)
โ”‚
โ””โ”€โ”€ Widgets
      โ”œโ”€โ”€ FrameSchedulerScope   โ† Lifecycle management widget
      โ”œโ”€โ”€ FpsOverlay            โ† Debug badge overlay
      โ”œโ”€โ”€ SchedulerBuilder      โ† Reactive zone-aware builder
      โ””โ”€โ”€ ScheduleOnce          โ† One-shot schedule-on-mount widget

๐Ÿ”‘ Core API Reference

SchedulerController.instance.schedule()

Future<void> schedule(
  Future<void> Function() task, {
  PriorityLevel priority = PriorityLevel.normal,  // Execution priority
  double estimatedDurationMs = 5.0,               // Budget hint (ms)
  String? id,                                     // Deduplication key
  int? maxWaitMs,                                 // Expiry timeout
  void Function()? onDropped,                     // Drop callback
})

SchedulerController.instance.runCritical()

// Bypasses ALL FPS checks โ€” use only for genuine emergencies
Future<void> runCritical(Future<void> Function() task)

Read-only properties

SchedulerController.instance.currentFps;        // double
SchedulerController.instance.currentZone;       // FpsZone
SchedulerController.instance.pendingTaskCount;  // int
SchedulerController.instance.metrics;           // SchedulerMetrics
SchedulerController.instance.isRunning;         // bool

๐ŸŽฎ Game Loop Integration Pattern

// In your game tick (called ~60 times/sec):
void _onGameTick() {
  final scheduler = SchedulerController.instance;

  // Physics: CRITICAL โ€” must never be skipped
  scheduler.schedule(
    () async => physicsEngine.integrate(dt),
    priority: PriorityLevel.critical,
    estimatedDurationMs: 2.0,
    id: 'physics_${frame}',
  );

  // Enemy AI: HIGH โ€” important but can wait one cycle
  if (frame % 10 == 0) {
    scheduler.schedule(
      () async => enemyAI.evaluate(gameState),
      priority: PriorityLevel.high,
      estimatedDurationMs: 6.0,
      id: 'ai_${frame}',
      maxWaitMs: 300,
    );
  }

  // Asset streaming: NORMAL โ€” fine to defer
  if (needsNewChunk) {
    scheduler.schedule(
      () async => worldStreamer.loadChunk(playerPosition),
      priority: PriorityLevel.normal,
      estimatedDurationMs: 35.0,
      id: 'chunk_${chunkId}',
      maxWaitMs: 8000,
    );
  }

  // Telemetry: LOW โ€” safe to drop
  scheduler.schedule(
    () async => telemetry.send({'fps': scheduler.currentFps}),
    priority: PriorityLevel.low,
    estimatedDurationMs: 1.0,
    maxWaitMs: 5000,
    onDropped: () => telemetry.discard(),
  );
}

๐Ÿ”ฌ How FPS is Measured

frame_scheduler uses SchedulerBinding.addTimingsCallback โ€” Flutter's official, lowest-level frame timing API. Each callback delivers a batch of FrameTiming objects, one per completed frame.

The FPS is computed using a rolling window average:

FPS = 1,000,000 ยตs รท mean(frame_durations_in_window)

Where each frame_duration is FrameTiming.totalSpan.inMicroseconds โ€” the full wall-clock time including both build and raster phases.

A larger window (configurable via SchedulerConfig.fpsWindowSize) produces smoother readings at the cost of slightly slower reaction to sudden drops.


๐Ÿงช Testing

# Run all tests
flutter test

# Run a specific test file
flutter test test/task_queue_test.dart

# Run benchmarks
dart run benchmark/scheduler_benchmark.dart

๐Ÿ“‹ Choosing the Right Priority โ€” Cheat Sheet

Scenario Priority
Responding to a user tap / button press critical
Playing a game sound effect critical
Updating a high-stakes UI element (health bar, score) critical
Loading the next scene's required assets high
Syncing game state to server high
Showing a toast/snackbar high
Pre-loading speculative assets normal
Non-critical background animations normal
Refreshing a feed in the background normal
Sending analytics / telemetry low
Background cache updates low
Prefetching content the user might see low

๐Ÿ“œ License

MIT License โ€” see LICENSE for details.


๐Ÿค Contributing

Contributions are welcome! Please open an issue first to discuss what you'd like to change.

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -m 'Add my feature'
  4. Push to the branch: git push origin feature/my-feature
  5. Open a Pull Request

๐Ÿ“ž Support

Libraries

frame_scheduler
frame_scheduler โ€” A production-grade, FPS-aware task scheduler for Flutter.