Skip to Content
DocsTinysavePerformance & Optimization

Performance & Optimization

TinySave is designed for performance, but understanding how it works helps you optimize save/load operations for your game. This guide covers performance monitoring, best practices, and optimization techniques.

Performance Counters

TinySave includes built-in performance monitoring via the PerfCounters class. After every save or load operation, you can inspect timing and size metrics.

PerfCounters Class

public class PerfCounters { public long CaptureMs { get; } // Time to capture scene state public long SerializeMs { get; } // Time to serialize to JSON public long IoMs { get; } // Time for disk I/O public long ResolveMs { get; } // Time to restore state (load only) public long TotalMs { get; } // Total operation time public long BytesWritten { get; } // Save file size in bytes }

Accessing Performance Data

using TinySave.Runtime; // After a save operation await SaveManager.SaveAsync("slot1"); PerfCounters perf = SaveManager.LastPerfCounters; Debug.Log($"Capture: {perf.CaptureMs}ms"); Debug.Log($"Serialize: {perf.SerializeMs}ms"); Debug.Log($"IO: {perf.IoMs}ms"); Debug.Log($"Total: {perf.TotalMs}ms"); Debug.Log($"File size: {perf.BytesWritten} bytes"); // After a load operation await SaveManager.LoadAsync("slot1"); perf = SaveManager.LastPerfCounters; Debug.Log($"IO: {perf.IoMs}ms"); Debug.Log($"Deserialize: {perf.SerializeMs}ms"); Debug.Log($"Restore: {perf.ResolveMs}ms"); Debug.Log($"Total: {perf.TotalMs}ms");

ToString() for Quick Logging

PerfCounters includes a convenient ToString() method:

await SaveManager.SaveAsync("slot1"); Debug.Log(SaveManager.LastPerfCounters.ToString()); // Output: // "TinySave Perf: Capture=5ms, Serialize=12ms, IO=8ms, Resolve=0ms, Total=25ms, Bytes=52481"

Save/Load Pipeline

Understanding the pipeline helps identify bottlenecks:

Save Pipeline

  1. Capture (Main Thread)

    • Scan scene for SaveID objects
    • Read [SaveField] values via reflection
    • Call ISaveCallbacks.OnBeforeSave()
    • Build SaveFile structure
  2. Serialize (Background Thread)

    • Convert SaveFile to JSON
    • Apply encryption if enabled
  3. I/O (Background Thread)

    • Write to temporary file
    • Atomic rename to final file

Load Pipeline

  1. I/O (Background Thread)

    • Read file from disk
    • Decrypt if encrypted
  2. Deserialize (Background Thread)

    • Parse JSON to SaveFile
    • Apply migrations if needed
  3. Restore (Main Thread)

    • Find SaveID objects in scene
    • Write values to [SaveField] fields
    • Call ISaveCallbacks.OnAfterLoad()

Performance Characteristics

Reflection Caching

TinySave caches reflection data to minimize overhead:

// First access per type: ~1-5ms (one-time cost) // Subsequent accesses: ~0.1ms (cached)

The reflection cache is built automatically when:

  • A component with [SaveField] is first encountered
  • Types are discovered during scene traversal

Threading

  • Capture and Restore run on the main thread (Unity requirement)
  • Serialization and I/O run on background threads
  • Async/await prevents blocking the game loop

Typical Performance

For reference, here are typical performance numbers:

Small game (10 objects, 50 fields): - Save: 15-30ms - Load: 10-20ms - File size: 5-10 KB Medium game (100 objects, 500 fields): - Save: 50-100ms - Load: 40-80ms - File size: 50-100 KB Large game (1000 objects, 5000 fields): - Save: 200-400ms - Load: 150-300ms - File size: 500-1000 KB

Performance varies based on:

  • Number of objects with SaveID
  • Number of [SaveField] fields
  • Complexity of data (nested collections, large strings)
  • Encryption enabled/disabled
  • Disk speed

Optimization Techniques

1. Mark Only Essential Fields

Only save what you need to restore game state:

// Bad: Saves everything public class Player : MonoBehaviour { [SaveField] public float health; [SaveField] public float maxHealth; [SaveField] public Vector3 velocity; // Recalculated, don't save [SaveField] public float jumpTimer; // Temporary state, don't save [SaveField] public List<Effect> activeEffects; // Large, optimize separately } // Good: Only saves essential state public class Player : MonoBehaviour { [SaveField] public float health; [SaveField] public float maxHealth; public Vector3 velocity; // Not saved, reset on load public float jumpTimer; // Not saved public List<Effect> activeEffects; // Use ISaveCustom for optimization }

2. Use ISaveCustom for Large Collections

Optimize large data structures with custom serialization:

// Before: Saves entire dictionary public class WorldState : MonoBehaviour { [SaveField] public Dictionary<string, ChunkData> chunks; // Could be huge! } // After: Save only modified chunks public class WorldState : MonoBehaviour, ISaveCustom { public Dictionary<string, ChunkData> chunks; private HashSet<string> modifiedChunks = new HashSet<string>(); public object CaptureState() { // Only save chunks that were modified var saveData = new Dictionary<string, ChunkData>(); foreach (string chunkId in modifiedChunks) { if (chunks.ContainsKey(chunkId)) saveData[chunkId] = chunks[chunkId]; } return saveData; } public void RestoreState(object state) { var saveData = state as Dictionary<string, ChunkData>; foreach (var kvp in saveData) { chunks[kvp.Key] = kvp.Value; } } }

3. Disable Auto-Save for Inactive Objects

Use TinySaveAutoSave to selectively disable saving:

// Disable saving for inactive enemies void OnEnemyDefeated() { var autoSave = GetComponent<TinySaveAutoSave>(); autoSave.AutoSaveEnabled = false; } // Or save destroyed state without full data void OnEnemyDefeated() { var autoSave = GetComponent<TinySaveAutoSave>(); autoSave.saveDestroyed = true; autoSave.ClearSelectedComponents(); // Don't save component data }

4. Compress String Data

If you save large strings, compress them manually:

using System.IO; using System.IO.Compression; using System.Text; public class CompressedData : MonoBehaviour, ISaveCustom { public string largeTextData; public object CaptureState() { byte[] bytes = Encoding.UTF8.GetBytes(largeTextData); using (var output = new MemoryStream()) { using (var gzip = new GZipStream(output, CompressionMode.Compress)) { gzip.Write(bytes, 0, bytes.Length); } return System.Convert.ToBase64String(output.ToArray()); } } public void RestoreState(object state) { byte[] compressed = System.Convert.FromBase64String((string)state); using (var input = new MemoryStream(compressed)) using (var gzip = new GZipStream(input, CompressionMode.Decompress)) using (var output = new MemoryStream()) { gzip.CopyTo(output); largeTextData = Encoding.UTF8.GetString(output.ToArray()); } } }

5. Batch Save Operations

If saving frequently, add a debounce timer:

public class AutoSaveController : MonoBehaviour { private float timeSinceLastSave; private bool pendingSave; void Update() { timeSinceLastSave += Time.deltaTime; // Save at most once every 30 seconds if (pendingSave && timeSinceLastSave >= 30f) { _ = PerformSave(); pendingSave = false; timeSinceLastSave = 0f; } } public void RequestSave() { pendingSave = true; } private async Task PerformSave() { await SaveManager.SaveAsync("autosave"); } }

6. Use Synchronous Save Sparingly

SaveSync() blocks the main thread - use only when necessary:

// Good: OnApplicationQuit where blocking is acceptable void OnApplicationQuit() { SaveManager.SaveSync("autosave"); } // Bad: Frequent blocking saves during gameplay void Update() { if (Input.GetKeyDown(KeyCode.S)) { SaveManager.SaveSync("quicksave"); // Blocks game! } } // Good: Async save during gameplay async void Update() { if (Input.GetKeyDown(KeyCode.S)) { await SaveManager.SaveAsync("quicksave"); // Non-blocking } }

7. Minimize SaveID Components

Only add SaveID to objects that actually need persistence:

// Bad: SaveID on every object GameObject particle = Instantiate(particlePrefab); particle.AddComponent<SaveID>(); // Particles don't need saving! // Good: SaveID only on persistent objects GameObject player = Instantiate(playerPrefab); player.AddComponent<SaveID>(); // Player needs saving GameObject enemy = Instantiate(enemyPrefab); enemy.AddComponent<SaveID>(); // Enemies need saving GameObject particle = Instantiate(particlePrefab); // No SaveID - particles are transient

Monitoring and Profiling

Log Performance Metrics

Create a performance monitoring system:

public class SavePerformanceMonitor : MonoBehaviour { private List<PerfCounters> saveHistory = new List<PerfCounters>(); void OnEnable() { SaveManager.BeforeSave += OnBeforeSave; } void OnDisable() { SaveManager.BeforeSave -= OnBeforeSave; } async void OnBeforeSave() { // Wait for save to complete (next frame) await Task.Yield(); PerfCounters perf = SaveManager.LastPerfCounters; saveHistory.Add(perf); // Log if save is slow if (perf.TotalMs > 100) { Debug.LogWarning($"Slow save detected: {perf}"); } // Keep last 100 saves if (saveHistory.Count > 100) saveHistory.RemoveAt(0); } public void PrintAveragePerformance() { if (saveHistory.Count == 0) return; float avgTotal = saveHistory.Average(p => p.TotalMs); float avgCapture = saveHistory.Average(p => p.CaptureMs); float avgSerialize = saveHistory.Average(p => p.SerializeMs); float avgIo = saveHistory.Average(p => p.IoMs); long avgBytes = (long)saveHistory.Average(p => p.BytesWritten); Debug.Log($"Average save performance over {saveHistory.Count} saves:"); Debug.Log($" Total: {avgTotal:F2}ms"); Debug.Log($" Capture: {avgCapture:F2}ms"); Debug.Log($" Serialize: {avgSerialize:F2}ms"); Debug.Log($" IO: {avgIo:F2}ms"); Debug.Log($" File size: {avgBytes} bytes"); } }

Unity Profiler Integration

You can profile TinySave operations with Unity’s Profiler:

using UnityEngine.Profiling; public class ProfiledSave : MonoBehaviour { async void SaveGame() { Profiler.BeginSample("TinySave.SaveAsync"); await SaveManager.SaveAsync("slot1"); Profiler.EndSample(); } }

Best Practices Summary

  1. Only save essential data - Don’t save calculated or transient values
  2. Use async/await - Prevent frame drops with asynchronous operations
  3. Implement ISaveCustom for large collections - Optimize complex data structures
  4. Monitor performance - Use PerfCounters to identify bottlenecks
  5. Cache references - Don’t repeatedly find components or objects
  6. Disable auto-save for inactive objects - Reduce data volume
  7. Batch saves - Don’t save on every frame
  8. Profile in builds - Editor performance doesn’t match build performance
  9. Test with real data - Profile with realistic save file sizes
  10. Consider platform differences - Mobile has slower disk I/O

Platform-Specific Considerations

Mobile (iOS/Android)

  • Disk I/O is slower than desktop
  • Use smaller save files when possible
  • Save less frequently
  • Test on real devices, not just editor

WebGL

  • Save to IndexedDB (handled automatically by Unity)
  • I/O can be slower than native platforms
  • Consider save file size limits (browser storage quotas)

Console (PlayStation, Xbox, Switch)

  • Follow platform-specific save guidelines
  • Use synchronous save on quit (async may not complete)
  • Test save/load during suspend/resume

Troubleshooting Performance Issues

Slow Capture (High CaptureMs)

Cause: Too many SaveID objects or complex ISaveCallbacks.

Solutions:

  • Reduce number of SaveID components
  • Optimize ISaveCallbacks.OnBeforeSave()
  • Use TinySaveAutoSave to selectively disable objects

Slow Serialization (High SerializeMs)

Cause: Large save files or complex nested data.

Solutions:

  • Reduce data volume with ISaveCustom
  • Compress large strings manually
  • Simplify data structures

Slow I/O (High IoMs)

Cause: Slow disk, large files, or encryption overhead.

Solutions:

  • Save to faster storage location
  • Reduce file size
  • Disable encryption for testing (minimal impact in practice)

Slow Restore (High ResolveMs)

Cause: Many objects to restore or complex ISaveCallbacks.

Solutions:

  • Optimize ISaveCallbacks.OnAfterLoad()
  • Reduce number of SaveID objects
  • Defer non-critical initialization

Next Steps

Last updated on