Skip to content

Instantly share code, notes, and snippets.

@ericallam
Created September 2, 2020 10:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericallam/294407c6c72574cea893044cfa1fe97e to your computer and use it in GitHub Desktop.
Save ericallam/294407c6c72574cea893044cfa1fe97e to your computer and use it in GitHub Desktop.
TextureFromFile leak post-mortem

TextureFromFile leak post-mortem

Usages of TextureFromFile in the game to load an image from StreamingAssets could lead to a memory leak of the loaded texture in the case of the TextureFromFile instance being cancelled before it finished.

This would happen in the case of the saga map collection view object reuse (e.g. SagaMapPlayStoryPolaroid) where a view object would be loaded and an initial TextureFromFile instance would get created, and then in the same frame that view object would be recycled and reused for a different section of the map, causing that original TextureFromFile instance to be cancelled.

The TextureFromFile.Cancel method looked like this:

public void Cancel()
{
    if (!_isRunning) return;

    _unityWebRequest.Abort();
}

It turns out, combining UnityWebRequest.Abort with the way TextureFromFile.TextureFromFilePathRoutine was implemented below leads to a memory leak:

private IEnumerator<float> TextureFromFilePathRoutine(IPendingPromise<Texture2D> promise) {
  using (_unityWebRequest = UnityWebRequestTexture.GetTexture($"file://{FilePath}"))
  {
      UnityWebRequestAsyncOperation asyncOperation = _unityWebRequest.SendWebRequest();

      _isRunning = true;

      yield return Timing.WaitUntilDone(asyncOperation);
      
      if (_unityWebRequest.isNetworkError || _unityWebRequest.isHttpError) {
          promise.Reject(new Exception($"TextureFromFile.TextureFromFilePathRoutine() failed to load with error: {_unityWebRequest.error}"));
      }
      else {
          Texture2D texture = DownloadHandlerTexture.GetContent(_unityWebRequest);

          if (texture == null)
          {
              string errorMessage = $"Texture failed to create for {FilePath}: {_unityWebRequest.error}";
              Exception exception = new Exception(errorMessage);
              promise.Reject(exception);
              yield break;
          }
          
          texture.name = Path.GetFileName(FilePath);
          
          promise.Resolve(texture);
      }

      _isRunning = false;
  }
}

Simply moving the Texture2D texture = DownloadHandlerTexture.GetContent(_unityWebRequest); line to just below the yield return Timing.WaitUntilDone(asyncOperation); like the below code does not fix the issue:

private IEnumerator<float> TextureFromFilePathRoutine(IPendingPromise<Texture2D> promise) {
  using (_unityWebRequest = UnityWebRequestTexture.GetTexture($"file://{FilePath}"))
  {
      UnityWebRequestAsyncOperation asyncOperation = _unityWebRequest.SendWebRequest();

      _isRunning = true;

      yield return Timing.WaitUntilDone(asyncOperation);

      Texture2D texture = DownloadHandlerTexture.GetContent(_unityWebRequest);
      
      if (_unityWebRequest.isNetworkError || _unityWebRequest.isHttpError) {
          promise.Reject(new Exception($"TextureFromFile.TextureFromFilePathRoutine() failed to load with error: {_unityWebRequest.error}"));
      }
      else {
          if (texture == null)
          {
              string errorMessage = $"Texture failed to create for {FilePath}: {_unityWebRequest.error}";
              Exception exception = new Exception(errorMessage);
              promise.Reject(exception);
              yield break;
          }
          
          texture.name = Path.GetFileName(FilePath);
          
          promise.Resolve(texture);
      }

      _isRunning = false;
  }
}

To fix the memory leak, I had to replace the use of UnityWebRequest.Abort with using an _isCancelled flag, and make sure to always call Texture2D texture = DownloadHandlerTexture.GetContent(_unityWebRequest); even if the process had been cancelled. The final, non-leaking code is below:

public void Cancel()
{
    if (!_isRunning) return;

    _isCancelled = true;
}

private IEnumerator<float> TextureFromFilePathRoutine(IPendingPromise<Texture2D> promise) {
    using (_unityWebRequest = UnityWebRequestTexture.GetTexture($"file://{FilePath}"))
    {
        UnityWebRequestAsyncOperation asyncOperation = _unityWebRequest.SendWebRequest();

        _isRunning = true;

        yield return Timing.WaitUntilDone(asyncOperation);
        
        Texture2D texture = DownloadHandlerTexture.GetContent(_unityWebRequest);

        if (_isCancelled)
        {
            promise.Reject(new Exception($"TextureFromFile.TextureFromFilePathRoutine() cancelled before it could complete loading the texture"));   
        }
        else if (_unityWebRequest.isNetworkError || _unityWebRequest.isHttpError) {
            promise.Reject(new Exception($"TextureFromFile.TextureFromFilePathRoutine() failed to load with error: {_unityWebRequest.error}"));
        }
        else {
            if (texture == null)
            {
                string errorMessage = $"Texture failed to create for {FilePath}: {_unityWebRequest.error}";
                Exception exception = new Exception(errorMessage);
                promise.Reject(exception);
                yield break;
            }
            
            texture.name = Path.GetFileName(FilePath);
            
            promise.Resolve(texture);
        }

        _isRunning = false;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment