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