001package arez.component.internal;
002
003import arez.ActionFlags;
004import arez.Arez;
005import arez.ArezContext;
006import arez.Component;
007import arez.ComputableValue;
008import arez.Disposable;
009import arez.SafeFunction;
010import arez.Task;
011import grim.annotations.OmitSymbol;
012import java.util.Arrays;
013import java.util.HashMap;
014import java.util.Map;
015import java.util.Objects;
016import java.util.Stack;
017import javax.annotation.Nonnull;
018import javax.annotation.Nullable;
019import org.intellij.lang.annotations.MagicConstant;
020import static org.realityforge.braincheck.Guards.*;
021
022/**
023 * The class responsible for caching ComputableValue instances for different input parameters.
024 */
025public final class MemoizeCache<T>
026  implements Disposable
027{
028  /**
029   * Functional interface for calculating memoizable value.
030   *
031   * @param <T> The type of the returned value.
032   */
033  @FunctionalInterface
034  public interface Function<T>
035  {
036    /**
037     * Return calculated memoizable value.
038     *
039     * @param args the functions arguments.
040     * @return the value generated by function.
041     */
042    T call( @Nonnull final Object... args );
043  }
044
045  /**
046   * Reference to the system to which this node belongs.
047   */
048  @OmitSymbol( unless = "arez.enable_zones" )
049  @Nullable
050  private final ArezContext _context;
051  /**
052   * A human consumable prefix for computable values. It should be non-null if {@link Arez#areNamesEnabled()} returns
053   * true and <code>null</code> otherwise.
054   */
055  @Nullable
056  @OmitSymbol( unless = "arez.enable_names" )
057  private final String _name;
058  /**
059   * The component that this memoize cache is contained within.
060   * This should only be set if {@link Arez#areNativeComponentsEnabled()} is true but can be null even if this is true.
061   */
062  @OmitSymbol( unless = "arez.enable_native_components" )
063  @Nullable
064  private final Component _component;
065  /**
066   * The function memoized.
067   */
068  @Nonnull
069  private final Function<T> _function;
070  /**
071   * The cache of all the ComputableValue created for each unique combination of parameters.
072   */
073  private final Map<Object, Object> _cache = new HashMap<>();
074  /**
075   * The number of arguments passed to memoized function.
076   */
077  private final int _argCount;
078  /**
079   * The flags passed to the created ComputableValues.
080   */
081  @MagicConstant( flagsFromClass = ComputableValue.Flags.class )
082  private final int _flags;
083  /**
084   * The index of the next ComputableValue created.
085   * This is only used when creating unique names for ComputableValues.
086   */
087  private int _nextIndex;
088  /**
089   * Flag indicating that the cache is currently being disposed.
090   */
091  private boolean _disposed;
092
093  /**
094   * Create the Memoize method cache.
095   *
096   * @param context   the context in which to create ComputableValue instances.
097   * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true.
098   * @param name      a human consumable prefix for computable values.
099   * @param function  the memoized function.
100   * @param argCount  the number of arguments expected to be passed to memoized function.
101   */
102  public MemoizeCache( @Nullable final ArezContext context,
103                       @Nullable final Component component,
104                       @Nullable final String name,
105                       @Nonnull final Function<T> function,
106                       final int argCount )
107  {
108    this( context, component, name, function, argCount, 0 );
109  }
110
111  /**
112   * Create the Memoize method cache.
113   *
114   * @param context   the context in which to create ComputableValue instances.
115   * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true.
116   * @param name      a human consumable prefix for computable values.
117   * @param function  the memoized function.
118   * @param argCount  the number of arguments expected to be passed to memoized function.
119   * @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}.
120   */
121  public MemoizeCache( @Nullable final ArezContext context,
122                       @Nullable final Component component,
123                       @Nullable final String name,
124                       @Nonnull final Function<T> function,
125                       final int argCount,
126                       @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) final int flags )
127  {
128    if ( Arez.shouldCheckApiInvariants() )
129    {
130      apiInvariant( () -> Arez.areZonesEnabled() || null == context,
131                    () -> "Arez-174: MemoizeCache passed a context but Arez.areZonesEnabled() is false" );
132      apiInvariant( () -> Arez.areNamesEnabled() || null == name,
133                    () -> "Arez-0159: MemoizeCache passed a name '" + name + "' but Arez.areNamesEnabled() is false" );
134      apiInvariant( () -> argCount > 0,
135                    () -> "Arez-0160: MemoizeCache constructed with invalid argCount: " + argCount +
136                          ". Expected positive value." );
137      final int mask = ComputableValue.Flags.PRIORITY_HIGHEST |
138                       ComputableValue.Flags.PRIORITY_HIGH |
139                       ComputableValue.Flags.PRIORITY_NORMAL |
140                       ComputableValue.Flags.PRIORITY_LOW |
141                       ComputableValue.Flags.PRIORITY_LOWEST |
142                       ComputableValue.Flags.NO_REPORT_RESULT |
143                       ComputableValue.Flags.AREZ_DEPENDENCIES |
144                       ComputableValue.Flags.AREZ_OR_NO_DEPENDENCIES |
145                       ComputableValue.Flags.AREZ_OR_EXTERNAL_DEPENDENCIES |
146                       ComputableValue.Flags.OBSERVE_LOWER_PRIORITY_DEPENDENCIES |
147                       ComputableValue.Flags.READ_OUTSIDE_TRANSACTION;
148
149      apiInvariant( () -> ( ~mask & flags ) == 0,
150                    () -> "Arez-0211: MemoizeCache passed unsupported flags. Unsupported bits: " + ( ~mask & flags ) );
151    }
152    _context = Arez.areZonesEnabled() ? Objects.requireNonNull( context ) : null;
153    _component = Arez.areNativeComponentsEnabled() ? component : null;
154    _name = Arez.areNamesEnabled() ? Objects.requireNonNull( name ) : null;
155    _function = Objects.requireNonNull( function );
156    _argCount = argCount;
157    _flags = flags;
158  }
159
160  /**
161   * Return the result of the memoized function, calculating if necessary.
162   *
163   * @param args the arguments passed to the memoized function.
164   * @return the result of the memoized function.
165   */
166  public T get( @Nonnull final Object... args )
167  {
168    if ( Arez.shouldCheckApiInvariants() )
169    {
170      apiInvariant( this::isNotDisposed,
171                    () -> "Arez-0161: MemoizeCache named '" + _name + "' had get() invoked when disposed." );
172    }
173    return getComputableValue( args ).get();
174  }
175
176  @Override
177  public boolean isDisposed()
178  {
179    return _disposed;
180  }
181
182  @Override
183  public void dispose()
184  {
185    if ( !_disposed )
186    {
187      _disposed = true;
188      getContext().safeAction( Arez.areNamesEnabled() ? _name : null, () -> {
189        disposeMap( _cache, _argCount );
190        _cache.clear();
191      }, ActionFlags.NO_VERIFY_ACTION_REQUIRED );
192    }
193  }
194
195  @Nonnull
196  private ArezContext getContext()
197  {
198    return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context();
199  }
200
201  /**
202   * Traverse to leaf map elements and dispose all contained ComputableValue instances.
203   */
204  @SuppressWarnings( "unchecked" )
205  private void disposeMap( @Nonnull final Map<Object, Object> map, final int depth )
206  {
207    if ( 1 == depth )
208    {
209      for ( final Map.Entry<Object, Object> entry : map.entrySet() )
210      {
211        final ComputableValue<?> computableValue = (ComputableValue<?>) entry.getValue();
212        computableValue.dispose();
213      }
214    }
215    else
216    {
217      for ( final Map.Entry<Object, Object> entry : map.entrySet() )
218      {
219        disposeMap( (Map<Object, Object>) entry.getValue(), depth - 1 );
220      }
221    }
222  }
223
224  /**
225   * Retrieve the computable value for specified parameters, creating it if necessary.
226   *
227   * @param args the arguments passed to the memoized function.
228   * @return the computable value instance for the specified args.
229   */
230  @SuppressWarnings( "unchecked" )
231  @Nonnull
232  public ComputableValue<T> getComputableValue( @Nonnull final Object... args )
233  {
234    if ( Arez.shouldCheckApiInvariants() )
235    {
236      apiInvariant( () -> args.length == _argCount,
237                    () -> "Arez-0162: MemoizeCache.getComputableValue called with " + args.length +
238                          " arguments but expected " + _argCount + " arguments." );
239    }
240    Map<Object, Object> map = _cache;
241    final int size = args.length - 1;
242    for ( int i = 0; i < size; i++ )
243    {
244      map = (Map<Object, Object>) map.computeIfAbsent( args[ i ], v -> new HashMap<>() );
245    }
246    ComputableValue<T> computableValue =
247      (ComputableValue<T>) map.computeIfAbsent( args[ size ], v -> createComputableValue( args ) );
248    if ( Disposable.isDisposed( computableValue ) )
249    {
250      computableValue = createComputableValue( args );
251      map.put( args[ size ], computableValue );
252    }
253    return computableValue;
254  }
255
256  /**
257   * Create computable value for specified parameters.
258   *
259   * @param args the arguments passed to the memoized function.
260   */
261  @Nonnull
262  private ComputableValue<T> createComputableValue( @Nonnull final Object... args )
263  {
264    final Component component = Arez.areNativeComponentsEnabled() ? _component : null;
265    final String name = Arez.areNamesEnabled() ? _name + "." + _nextIndex++ : null;
266    final SafeFunction<T> function = () -> {
267      Arez.context().registerHook( "$MC$", null, () -> disposeComputableValue( args ) );
268      return _function.call( args );
269    };
270    return getContext().computable( component, name, function, _flags );
271  }
272
273  /**
274   * Method invoked to dispose memoized value.
275   * This is called from deactivate hook so there should always by a cached value present
276   * and thus we never check for missing elements in chain.
277   *
278   * @param args the arguments originally passed to the memoized function.
279   */
280  @SuppressWarnings( "unchecked" )
281  void disposeComputableValue( @Nonnull final Object... args )
282  {
283    if ( Arez.shouldCheckInvariants() )
284    {
285      invariant( () -> args.length == _argCount,
286                 () -> "Arez-0163: MemoizeCache.disposeComputableValue called with " + args.length +
287                       " argument(s) but expected " + _argCount + " argument(s)." );
288    }
289    if ( _disposed )
290    {
291      return;
292    }
293    final Stack<Map<Object, ?>> stack = new Stack<>();
294    stack.push( _cache );
295    final int size = args.length - 1;
296    for ( int i = 0; i < size; i++ )
297    {
298      stack.push( (Map<Object, ?>) stack.peek().get( args[ i ] ) );
299    }
300    final ComputableValue<T> computableValue = (ComputableValue<T>) stack.peek().remove( args[ size ] );
301    if ( Arez.shouldCheckInvariants() )
302    {
303      invariant( () -> null != computableValue,
304                 () -> "Arez-0193: MemoizeCache.disposeComputableValue called with args " + Arrays.asList( args ) +
305                       " but unable to locate corresponding ComputableValue." );
306    }
307    assert null != computableValue;
308    getContext().task( Arez.areNamesEnabled() ? computableValue.getName() + ".dispose" : null,
309                       computableValue::dispose,
310                       Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK );
311    while ( stack.size() > 1 )
312    {
313      final Map<Object, ?> map = stack.pop();
314      if ( map.isEmpty() )
315      {
316        stack.peek().remove( args[ stack.size() - 1 ] );
317      }
318      else
319      {
320        return;
321      }
322    }
323  }
324
325  @OmitSymbol
326  Map<Object, Object> getCache()
327  {
328    return _cache;
329  }
330
331  @OmitSymbol
332  int getNextIndex()
333  {
334    return _nextIndex;
335  }
336
337  @OmitSymbol
338  int getFlags()
339  {
340    return _flags;
341  }
342}