object_pool 1.0.0 copy "object_pool: ^1.0.0" to clipboard
object_pool: ^1.0.0 copied to clipboard

A high-performance generic object pool for Dart and Flutter that reduces garbage collection pressure by reusing pre-allocated objects, ideal for games, animations, particle systems, and performance-cr [...]

object_pool 🎯 #

pub.flutter-io.cn Dart SDK License: MIT style: lints

A high-performance, generic object pool for Dart and Flutter that eliminates Garbage Collector (GC) pressure by reusing pre-allocated objects instead of creating and destroying them on every request.


image

The Problem #

Dart's GC must pause your app to collect short-lived objects. When you create thousands of objects per second — common in games, particle systems, and real-time data processing — GC pauses cause visible stuttering (jank) in Flutter UIs.

// ❌ This causes GC pressure — 60,000 heap allocations/sec at 60fps
void onFrame() {
  for (var i = 0; i < 1000; i++) {
    final p = Particle(); // ← heap allocation
    p.init(x: i.toDouble(), y: 0);
    // p goes out of scope → GC must collect it → frame drops
  }
}

The Solution #

// ✅ Zero GC pressure — zero allocations after warmup
final pool = ObjectPool<Particle>(
  factory: () => Particle(),
  initialSize: 1000, // pre-allocate once
);

void onFrame() {
  for (var i = 0; i < 1000; i++) {
    final p = pool.acquire()!; // O(1), no allocation
    p.init(x: i.toDouble(), y: 0);
    // ... use p ...
    pool.release(p);           // O(1), no deallocation/GC
  }
}

Installation #

Add to your pubspec.yaml:

dependencies:
  object_pool: ^1.0.0

Then run:

dart pub get
# or
flutter pub get

Quick Start #

import 'package:object_pool/object_pool.dart';

// 1. Create the pool
final pool = ObjectPool<MyObject>(
  factory: () => MyObject(),
  initialSize: 50,      // pre-allocate 50 objects
  maxSize: 200,         // grow up to 200 if needed
);

// 2a. Manual acquire/release
final obj = pool.acquire();
if (obj != null) {
  obj.doWork();
  pool.release(obj);  // always release!
}

// 2b. Scoped use() — RECOMMENDED (auto-releases even on exception)
pool.use((obj) {
  obj.doWork();
}); // obj automatically released here

// 2c. Async scoped
await pool.useAsync((obj) async {
  await obj.fetchData();
});

// 3. Monitor performance
print(pool.metrics); // utilization, hitRate, acquires/sec, etc.

// 4. Dispose when done
pool.dispose();

Implement Poolable to hook into the pool's acquire/release lifecycle:

class Particle implements Poolable {
  double x = 0, y = 0;
  double velocityX = 0, velocityY = 0;
  double alpha = 1.0;
  bool active = false;

  void init({required double x, required double y}) {
    this.x = x; this.y = y;
  }

  @override
  void onAcquire() {
    active = true; // called just before object is returned to caller
  }

  @override
  void onRelease() {
    // called when returned to pool — RESET ALL STATE here
    x = 0; y = 0; velocityX = 0; velocityY = 0;
    alpha = 1.0; active = false;
  }

  @override
  bool get isValid => true; // return false for broken/expired objects
}

Overflow Strategies #

What happens when all objects are in use? Choose your strategy:

// Default: grow the pool (safe for most cases)
final pool = ObjectPool<T>(
  factory: () => T(),
  strategy: ExpandStrategy(maxSize: 500, growthIncrement: 10),
);

// Drop the request — return null (best for particles, sound FX)
final pool = ObjectPool<T>(
  factory: () => T(),
  strategy: const DiscardStrategy(),
);

// Reclaim the oldest in-use object (best for sound channels)
final pool = ObjectPool<T>(
  factory: () => T(),
  strategy: const ReplaceStrategy(),
);
Strategy Exhaustion Behavior acquire() Returns Best For
ExpandStrategy Creates new objects Non-null (new object) General use ✅
DiscardStrategy Does nothing null Particles, audio FX
ReplaceStrategy Reclaims oldest Non-null (reclaimed) Sound channels, VFX

Performance Tuning #

final m = pool.metrics;

// Utilization: fraction of pool currently in use (0.0–1.0)
// > 0.85 sustained → increase initialSize or maxSize
print('Utilization: ${(m.utilizationRate * 100).toStringAsFixed(1)}%');

// Hit rate: fraction of acquires served from existing pool
// < 0.95 → increase initialSize to reduce expansions
print('Hit rate: ${(m.hitRate * 100).toStringAsFixed(1)}%');

// Acquires per second — helps size the pool for peak load
print('Acq/sec: ${m.acquiresPerSecond.toStringAsFixed(0)}');

// Exhaustion count — how often requests were dropped (DiscardStrategy)
print('Drops: ${m.exhaustionCount}');

// Peak active — highest simultaneous usage; use as initialSize guide
print('Peak active: ${m.peakActiveCount}');

API Reference #

ObjectPool<T> Constructor #

Parameter Type Default Description
factory T Function() required Creates new instances of T
initialSize int 10 Objects to pre-allocate at startup
maxSize int? null Hard cap (used by default ExpandStrategy)
strategy PoolOverflowStrategy<T>? ExpandStrategy Exhaustion behavior
validateOnAcquire bool false Check Poolable.isValid before returning
resetOnRelease bool true Call Poolable.onRelease automatically

Methods #

Method Returns Description
acquire() T? Get an object; null if exhausted + strategy returns null
acquireOrThrow() T Get an object; throws PoolExhaustedException on failure
release(T) void Return an object to the pool
releaseIfNotNull(T?) void Release if not null (convenience)
use<R>(R Function(T)) R? Scoped acquire+release (sync)
useAsync<R>(Future<R> Function(T)) Future<R?> Scoped acquire+release (async)
prewarm(int) void Ensure pool has at least N objects ready
drain() void Remove all idle objects (free memory)
dispose() void Destroy the pool and all objects

Properties #

Property Type Description
metrics PoolMetrics Performance statistics snapshot
size int Total objects (in-use + available)
activeCount int Objects currently checked out
availableCount int Objects ready for acquisition
hasAvailable bool Whether at least one object is free

Common Use Cases #

🎮 Game — Particle System #

class Particle implements Poolable { /* ... */ }

final particlePool = ObjectPool<Particle>(
  factory: Particle.new,
  initialSize: 500,
  maxSize: 1000,
  strategy: const DiscardStrategy(), // drop excess particles gracefully
);

// On explosion:
void spawnExplosion(double x, double y) {
  for (var i = 0; i < 50; i++) {
    final p = particlePool.acquire();
    if (p == null) return; // pool full — skip gracefully
    p.init(x: x, y: y, angle: Random().nextDouble() * 6.28);
    activeParticles.add(p);
  }
}

// Each frame:
void update() {
  activeParticles.removeWhere((p) {
    p.update();
    if (!p.alive) {
      particlePool.release(p); // O(1) — no GC
      return true;
    }
    return false;
  });
}

🌐 Network — Connection Pool #

class HttpConnection implements Poolable {
  bool _valid = true;

  Future<Response> get(String url) async { /* ... */ }

  @override void onAcquire() {}
  @override void onRelease() {} // keep TCP connection alive
  @override bool get isValid => _valid;
}

final httpPool = ObjectPool<HttpConnection>(
  factory: HttpConnection.new,
  initialSize: 10,
  maxSize: 50,
  validateOnAcquire: true, // auto-replace broken connections
);

Future<Response> fetch(String url) async {
  return await httpPool.useAsync((conn) => conn.get(url))
    ?? Response.error(503);
}

🎵 Audio — Sound Channel Pool #

final soundPool = ObjectPool<SoundChannel>(
  factory: SoundChannel.new,
  initialSize: 32,
  maxSize: 32,
  strategy: const ReplaceStrategy(), // new sounds replace oldest
);

void playSfx(String asset) {
  soundPool.use((channel) => channel.play(asset));
}

🏗️ Prewarm Before Peak Load #

// During Flutter app splash screen / level loading:
Future<void> loadResources() async {
  particlePool.prewarm(1000); // allocate 1000 particles NOW, not during gameplay
  await Future<void>.delayed(const Duration(seconds: 2)); // loading animation
}

Common Mistakes to Avoid #

❌ Using an object after releasing it #

final obj = pool.acquire()!;
pool.release(obj);
obj.x = 10; // DANGER: obj may now belong to another caller!
// ✅ Use scoped pattern — impossible to use after release
pool.use((obj) {
  obj.x = 10; // safe — can't escape this scope
});

❌ Forgetting to release (pool leak) #

Future<void> bad() async {
  final obj = pool.acquire()!;
  await riskyOperation(); // throws → release never called → leak!
  pool.release(obj);
}
// ✅ useAsync guarantees release even on exception
Future<void> good() async {
  await pool.useAsync((obj) => riskyOperation());
}

❌ Pool too small for peak load #

// ❌ Causes constant expansions → defeats the purpose
final pool = ObjectPool<Particle>(factory: Particle.new, initialSize: 5);

// ✅ Profile peak usage with metrics.peakActiveCount, then size accordingly
final pool = ObjectPool<Particle>(
  factory: Particle.new,
  initialSize: 200, // observed peak × 1.25
);

Performance Expectations #

On a typical modern device with HeavyObject (~200 bytes):

Scenario Naive (alloc/GC) Object Pool Speedup
Single acquire/release ~600 ns ~60 ns ~10×
1000/frame at 60fps ~36 ms/s GC ~0 ms GC ∞× GC events
Peak memory pressure High (fragmented) Low (fixed block) Significant

The biggest win is not raw speed — it's eliminating GC pauses entirely. Each GC pause that exceeds 16ms drops a frame and causes visible jank. Object pools make GC pauses from short-lived objects structurally impossible.


Internal Architecture #

ObjectPool<T>
│
├── _entries: List<PoolEntry<T>>     ← all objects, in-use + available
├── _entryMap: HashMap.identity()    ← O(1) object → entry lookup
└── _freeListHead: PoolEntry<T>?     ← head of embedded free list
                                        │
    PoolEntry<T>                        ▼
    ├── object: T                   [Entry0] → [Entry3] → [Entry1] → null
    ├── inUse: bool                  free     free        free
    ├── nextFree: PoolEntry<T>?
    ├── lastAcquiredAt: DateTime?
    └── reuseCount: int

The embedded free list is the key insight: available entries form a linked list that threads through the entries themselves via nextFree. No separate data structure is needed. Both acquire() (pop head) and release() (push head) are O(1).


Changelog #

See CHANGELOG.md.

License #

MIT — See LICENSE.

Contributing #

Issues and PRs welcome at the GitHub repository.

Please run dart test and dart analyze before submitting a pull request.

0
likes
150
points
5
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A high-performance generic object pool for Dart and Flutter that reduces garbage collection pressure by reusing pre-allocated objects, ideal for games, animations, particle systems, and performance-critical applications.

Repository (GitHub)
View/report issues

Topics

#performance #games #memory #optimization #pooling

License

MIT (license)

Dependencies

meta

More

Packages that depend on object_pool