rough-native
Version:
Create graphics using HTML Canvas or SVG with a hand-drawn, sketchy, appearance. Features comprehensive React hooks, memory management, and React 18 concurrent rendering support.
283 lines (231 loc) • 8.15 kB
Markdown
This document describes the memory pressure detection and adaptive caching system implemented in rough-native to prevent app crashes on memory-constrained devices.
React Native apps run on devices with varying memory constraints:
- **Low-end Android devices**: 1-2GB RAM
- **Mid-range devices**: 2-4GB RAM
- **High-end devices**: 6-12GB RAM
Fixed cache sizes can cause:
- **App crashes** when OS kills the app for excessive memory usage
- **Performance degradation** from memory pressure
- **Cache thrashing** on low-memory devices
Centralized memory pressure detection:
```typescript
class MemoryMonitor {
private memoryPressureLevel: 'low' | 'medium' | 'high' = 'low';
getMemoryPressureLevel(): 'low' | 'medium' | 'high' {
// Checks memory every 5 seconds to avoid overhead
this.checkMemoryPressure();
return this.memoryPressureLevel;
}
getRecommendedCacheSize(): number {
switch (this.memoryPressureLevel) {
case 'high': return 50; // Conservative for low-memory devices
case 'medium': return 200; // Balanced for typical devices
case 'low': return 500; // Generous for high-memory devices
}
}
}
```
```typescript
if (global.performance?.memory) {
const memory = global.performance.memory;
const usedRatio = memory.usedJSHeapSize / memory.jsHeapSizeLimit;
if (usedRatio > 0.8) {
this.memoryPressureLevel = 'high';
} else if (usedRatio > 0.6) {
this.memoryPressureLevel = 'medium';
} else {
this.memoryPressureLevel = 'low';
}
}
```
```typescript
if (performance.memory) {
// Same logic as above for web environments
}
```
```typescript
// Detect low-end Android devices via user agent
const isLowEndDevice = /Android.*4\.|Android.*[0-3]\./i.test(userAgent);
if (isLowEndDevice) {
this.memoryPressureLevel = 'high';
}
```
```typescript
class DeepEqualityCache {
private get maxCacheSize(): number {
return this.memoryMonitor.getRecommendedCacheSize();
}
set(a: any, b: any, result: boolean): void {
// Adaptive cache size based on memory pressure
if (this.cacheCount > this.maxCacheSize) {
this.cache = new WeakMap();
this.cacheCount = 0;
// Aggressive cleanup under high pressure
if (this.memoryMonitor.shouldAgressivelyCleanup()) {
if (global.gc) global.gc(); // Force GC if available
}
}
// Skip bidirectional caching under memory pressure
if (!this.memoryMonitor.shouldAgressivelyCleanup()) {
// Cache both directions for efficiency
}
}
}
```
```typescript
class ShapeGenerationStore {
generateShape(key: string, generator: () => any): any {
// Limit concurrent generations under memory pressure
if (this.memoryMonitor.shouldAgressivelyCleanup() &&
this.pendingGenerations.size > 2) {
return null; // Defer generation
}
// Proactive cleanup before generating
if (this.cache.size >= this.maxCacheSize) {
this.performMemoryPressureCleanup();
}
}
private performMemoryPressureCleanup(): void {
const pressureLevel = this.memoryMonitor.getMemoryPressureLevel();
if (pressureLevel === 'high') {
// Keep only 25% of cache (most recent)
const entries = Array.from(this.cache.entries());
const toKeep = entries.slice(-Math.floor(entries.length * 0.25));
this.cache.clear();
toKeep.forEach(([key, value]) => this.cache.set(key, value));
}
}
}
```
- **Cache size**: 500 items (generous)
- **Bidirectional caching**: Enabled
- **Concurrent generations**: Unlimited
- **Cleanup strategy**: Standard size limits
- **Cache size**: 200 items (balanced)
- **Bidirectional caching**: Enabled
- **Concurrent generations**: Normal limits
- **Cleanup strategy**: 50% cache reduction when full
- **Cache size**: 50 items (conservative)
- **Bidirectional caching**: Disabled
- **Concurrent generations**: Limited to 2
- **Cleanup strategy**: 75% cache reduction when full
- **Garbage collection**: Force GC when available
```typescript
import { debugUtils } from 'rough-native';
// Check current memory pressure
const pressure = debugUtils.getMemoryPressure(); // 'low' | 'medium' | 'high'
// Get recommended cache size for current conditions
const cacheSize = debugUtils.getRecommendedCacheSize(); // 50, 200, or 500
// Get enhanced cache statistics
const stats = debugUtils.getDeepEqualStats();
console.log(stats);
// {
// hits: 1250,
// misses: 89,
// hitRate: 0.93,
// memoryPressure: 'medium',
// maxCacheSize: 200,
// currentCacheCount: 156
// }
// Force cleanup under memory pressure
debugUtils.forceMemoryCleanup();
```
```typescript
// Monitor memory pressure in production
useEffect(() => {
const interval = setInterval(() => {
const pressure = debugUtils.getMemoryPressure();
if (pressure === 'high') {
// Log to analytics service
analytics.track('memory_pressure_high', {
cacheSize: debugUtils.getShapeCacheSize(),
platform: Platform.OS
});
}
}, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, []);
```
```typescript
// Automatically detects and adapts:
// - Cache size: 50 items max
// - Aggressive cleanup enabled
// - Limited concurrent operations
// - Force GC when available
```
```typescript
// Balanced approach:
// - Cache size: 200 items max
// - Standard cleanup strategy
// - Normal concurrent operations
```
```typescript
// Performance optimized:
// - Cache size: 500 items max
// - Generous caching enabled
// - Full concurrent operations
```
| Memory Condition | Before | After |
|-----------------|--------|-------|
| Low memory device | App crash risk | Adaptive caching prevents crashes |
| Memory pressure spike | Fixed cache size | 75% cache reduction |
| Background app return | Stale large cache | Reduced cache size |
| Concurrent generation | Unlimited | Limited during pressure |
```typescript
// Override memory detection (for testing)
process.env.ROUGH_NATIVE_MEMORY_PRESSURE = 'high'; // 'low' | 'medium' | 'high'
// Disable automatic GC calls
process.env.ROUGH_NATIVE_DISABLE_GC = 'true';
// Custom cache size override
process.env.ROUGH_NATIVE_CACHE_SIZE = '100';
```
```typescript
// For apps with specific memory requirements
const customMemoryMonitor = {
getMemoryPressureLevel: () => 'high', // Always conservative
getRecommendedCacheSize: () => 25, // Very small cache
shouldAgressivelyCleanup: () => true // Always aggressive
};
```
- **Eliminated memory-related crashes** on low-end devices
- **Adaptive behavior** prevents OS app termination
- **Graceful degradation** under memory pressure
- **Device-appropriate caching** maximizes performance
- **Memory-aware cleanup** prevents cache thrashing
- **Smart concurrent limiting** prevents memory spikes
- **Automatic adaptation** requires no configuration
- **Rich debugging tools** for memory monitoring
- **Production analytics** for memory pressure tracking
The memory pressure detection system ensures rough-native works reliably across the full spectrum of React Native devices, from budget Android phones to high-end flagship devices.