All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arez.component.internal.MemoizeCache Maven / Gradle / Ivy

There is a newer version: 0.213
Show newest version
package arez.component.internal;

import arez.ActionFlags;
import arez.Arez;
import arez.ArezContext;
import arez.Component;
import arez.ComputableValue;
import arez.Disposable;
import arez.Procedure;
import arez.SafeFunction;
import arez.Task;
import grim.annotations.OmitSymbol;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.intellij.lang.annotations.MagicConstant;
import static org.realityforge.braincheck.Guards.*;

/**
 * The class responsible for caching
 */
public final class MemoizeCache
  implements Disposable
{
  /**
   * Functional interface for calculating memoizable value.
   *
   * @param  The type of the returned value.
   */
  @FunctionalInterface
  public interface Function
  {
    /**
     * Return calculated memoizable value.
     *
     * @param args the functions arguments.
     * @return the value generated by function.
     */
    T call( @Nonnull final Object... args );
  }

  /**
   * Reference to the system to which this node belongs.
   */
  @OmitSymbol( unless = "arez.enable_zones" )
  @Nullable
  private final ArezContext _context;
  /**
   * A human consumable prefix for computable values. It should be non-null if {@link Arez#areNamesEnabled()} returns
   * true and null otherwise.
   */
  @Nullable
  @OmitSymbol( unless = "arez.enable_names" )
  private final String _name;
  /**
   * The component that this memoize cache is contained within.
   * This should only be set if {@link Arez#areNativeComponentsEnabled()} is true but can be null even if this is true.
   */
  @OmitSymbol( unless = "arez.enable_native_components" )
  @Nullable
  private final Component _component;
  /**
   * The function memoized.
   */
  @Nonnull
  private final Function _function;
  /**
   * The cache of all the ComputableValue created for each unique combination of parameters.
   */
  private final Map _cache = new HashMap<>();
  /**
   * The number of arguments passed to memoized function.
   */
  private final int _argCount;
  /**
   * The flags passed to the created ComputableValues.
   */
  private final int _flags;
  /**
   * The index of the next ComputableValue created.
   * This is only used when creating unique names for ComputableValues.
   */
  private int _nextIndex;
  /**
   * Flag indicating that the cache is currently being disposed.
   */
  private boolean _disposed;

  /**
   * Create the Memoize method cache.
   *
   * @param context   the context in which to create ComputableValue instances.
   * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true.
   * @param name      a human consumable prefix for computable values.
   * @param function  the memoized function.
   * @param argCount  the number of arguments expected to be passed to memoized function.
   */
  public MemoizeCache( @Nullable final ArezContext context,
                       @Nullable final Component component,
                       @Nullable final String name,
                       @Nonnull final Function function,
                       final int argCount )
  {
    this( context, component, name, function, argCount, 0 );
  }

  /**
   * Create the Memoize method cache.
   *
   * @param context   the context in which to create ComputableValue instances.
   * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true.
   * @param name      a human consumable prefix for computable values.
   * @param function  the memoized function.
   * @param argCount  the number of arguments expected to be passed to memoized function.
   * @param flags     the flags that are used when creating ComputableValue instances. The only flags supported are flags defined in {@link ComputableValue.Flags} except for {@link ComputableValue.Flags#KEEPALIVE}, {@link ComputableValue.Flags#RUN_NOW} and {@link ComputableValue.Flags#RUN_LATER}.
   */
  public MemoizeCache( @Nullable final ArezContext context,
                       @Nullable final Component component,
                       @Nullable final String name,
                       @Nonnull final Function function,
                       final int argCount,
                       @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) final int flags )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> Arez.areZonesEnabled() || null == context,
                    () -> "Arez-174: MemoizeCache passed a context but Arez.areZonesEnabled() is false" );
      apiInvariant( () -> Arez.areNamesEnabled() || null == name,
                    () -> "Arez-0159: MemoizeCache passed a name '" + name + "' but Arez.areNamesEnabled() is false" );
      apiInvariant( () -> argCount > 0,
                    () -> "Arez-0160: MemoizeCache constructed with invalid argCount: " + argCount +
                          ". Expected positive value." );
      final int mask = ComputableValue.Flags.PRIORITY_HIGHEST |
                       ComputableValue.Flags.PRIORITY_HIGH |
                       ComputableValue.Flags.PRIORITY_NORMAL |
                       ComputableValue.Flags.PRIORITY_LOW |
                       ComputableValue.Flags.PRIORITY_LOWEST |
                       ComputableValue.Flags.NO_REPORT_RESULT |
                       ComputableValue.Flags.AREZ_DEPENDENCIES |
                       ComputableValue.Flags.AREZ_OR_NO_DEPENDENCIES |
                       ComputableValue.Flags.AREZ_OR_EXTERNAL_DEPENDENCIES |
                       ComputableValue.Flags.OBSERVE_LOWER_PRIORITY_DEPENDENCIES |
                       ComputableValue.Flags.READ_OUTSIDE_TRANSACTION;

      apiInvariant( () -> ( ~mask & flags ) == 0,
                    () -> "Arez-0211: MemoizeCache passed unsupported flags. Unsupported bits: " + ( ~mask & flags ) );
    }
    _context = Arez.areZonesEnabled() ? Objects.requireNonNull( context ) : null;
    _component = Arez.areNativeComponentsEnabled() ? component : null;
    _name = Arez.areNamesEnabled() ? Objects.requireNonNull( name ) : null;
    _function = Objects.requireNonNull( function );
    _argCount = argCount;
    _flags = flags;
  }

  /**
   * Return the result of the memoized function, calculating if necessary.
   *
   * @param args the arguments passed to the memoized function.
   * @return the result of the memoized function.
   */
  public T get( @Nonnull final Object... args )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( this::isNotDisposed,
                    () -> "Arez-0161: MemoizeCache named '" + _name + "' had get() invoked when disposed." );
    }
    return getComputableValue( args ).get();
  }

  @Override
  public boolean isDisposed()
  {
    return _disposed;
  }

  @Override
  public void dispose()
  {
    if ( !_disposed )
    {
      _disposed = true;
      getContext().safeAction( Arez.areNamesEnabled() ? _name : null, () -> {
        disposeMap( _cache, _argCount );
        _cache.clear();
      }, ActionFlags.NO_VERIFY_ACTION_REQUIRED );
    }
  }

  @Nonnull
  private ArezContext getContext()
  {
    return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context();
  }

  /**
   * Traverse to leaf map elements and dispose all contained ComputableValue instances.
   */
  @SuppressWarnings( "unchecked" )
  private void disposeMap( @Nonnull final Map map, final int depth )
  {
    if ( 1 == depth )
    {
      for ( final Map.Entry entry : map.entrySet() )
      {
        final ComputableValue computableValue = (ComputableValue) entry.getValue();
        computableValue.dispose();
      }
    }
    else
    {
      for ( final Map.Entry entry : map.entrySet() )
      {
        disposeMap( (Map) entry.getValue(), depth - 1 );
      }
    }
  }

  /**
   * Retrieve the computable value for specified parameters, creating it if necessary.
   *
   * @param args the arguments passed to the memoized function.
   * @return the computable value instance for the specified args.
   */
  @SuppressWarnings( "unchecked" )
  @Nonnull
  public ComputableValue getComputableValue( @Nonnull final Object... args )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> args.length == _argCount,
                    () -> "Arez-0162: MemoizeCache.getComputableValue called with " + args.length +
                          " arguments but expected " + _argCount + " arguments." );
    }
    Map map = _cache;
    final int size = args.length - 1;
    for ( int i = 0; i < size; i++ )
    {
      map = (Map) map.computeIfAbsent( args[ i ], v -> new HashMap<>() );
    }
    ComputableValue computableValue =
      (ComputableValue) map.computeIfAbsent( args[ size ], v -> createComputableValue( args ) );
    if ( Disposable.isDisposed( computableValue ) )
    {
      computableValue = createComputableValue( args );
      map.put( args[ size ], computableValue );
    }
    return computableValue;
  }

  /**
   * Create computable value for specified parameters.
   *
   * @param args the arguments passed to the memoized function.
   */
  @Nonnull
  private ComputableValue createComputableValue( @Nonnull final Object... args )
  {
    final Component component = Arez.areNativeComponentsEnabled() ? _component : null;
    final String name = Arez.areNamesEnabled() ? _name + "." + _nextIndex++ : null;
    final Procedure onDeactivate = () -> disposeComputableValue( args );
    final SafeFunction function = () -> _function.call( args );
    return getContext().computable( component, name, function, null, onDeactivate, _flags );
  }

  /**
   * Method invoked to dispose memoized value.
   * This is called from deactivate hook so there should always by a cached value present
   * and thus we never check for missing elements in chain.
   *
   * @param args the arguments originally passed to the memoized function.
   */
  @SuppressWarnings( "unchecked" )
  void disposeComputableValue( @Nonnull final Object... args )
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> args.length == _argCount,
                 () -> "Arez-0163: MemoizeCache.disposeComputableValue called with " + args.length +
                       " argument(s) but expected " + _argCount + " argument(s)." );
    }
    if ( _disposed )
    {
      return;
    }
    final Stack> stack = new Stack<>();
    stack.push( _cache );
    final int size = args.length - 1;
    for ( int i = 0; i < size; i++ )
    {
      stack.push( (Map) stack.peek().get( args[ i ] ) );
    }
    final ComputableValue computableValue = (ComputableValue) stack.peek().remove( args[ size ] );
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> null != computableValue,
                 () -> "Arez-0193: MemoizeCache.disposeComputableValue called with args " + Arrays.asList( args ) +
                       " but unable to locate corresponding ComputableValue." );
    }
    assert null != computableValue;
    getContext().task( Arez.areNamesEnabled() ? computableValue.getName() + ".dispose" : null,
                       computableValue::dispose,
                       Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK );
    while ( stack.size() > 1 )
    {
      final Map map = stack.pop();
      if ( map.isEmpty() )
      {
        stack.peek().remove( args[ stack.size() - 1 ] );
      }
      else
      {
        return;
      }
    }
  }

  @OmitSymbol
  Map getCache()
  {
    return _cache;
  }

  @OmitSymbol
  int getNextIndex()
  {
    return _nextIndex;
  }

  @OmitSymbol
  int getFlags()
  {
    return _flags;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy