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