diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java index 2c93aed0c5..fb450d3320 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java @@ -12,6 +12,8 @@ import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects; import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.pure; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profile; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profileEffects; import static java.util.stream.Collectors.joining; /** @@ -62,7 +64,10 @@ private void augmentEffectName(DynamicsEffect effect) { } }; if (MutableResourceFlags.DETECT_BUSY_CELLS) { - result = Profiling.profileEffects(result); + result = profileEffects(result); + } + if (MutableResourceFlags.PROFILE_GET_DYNAMICS) { + result = profile(result); } return result; } @@ -79,12 +84,12 @@ static > void set(MutableResource resource, Expiring * Turn on busy cell detection. * *

- * Calling this method once before constructing your model will profile effects on every cell. + * Calling this method once before constructing your model will profile effects on every resource. * Profiling effects may be compute and/or memory intensive, and should not be used in production. *

*

- * If only a few cells are suspect, you can also call {@link Profiling#profileEffects} - * directly on just those cells, rather than profiling every cell. + * If only a few resources are suspect, you can also call {@link Profiling#profileEffects} + * directly on just those resource, rather than profiling every resource. *

*

* Call {@link Profiling#dump()} to see results. @@ -93,6 +98,27 @@ static > void set(MutableResource resource, Expiring static void detectBusyCells() { MutableResourceFlags.DETECT_BUSY_CELLS = true; } + + /** + * Turn on profiling for all {@link MutableResource}s created by {@link MutableResource#resource}. + * Also implies {@link MutableResource#detectBusyCells()}. + * + *

+ * Calling this method once before constructing your model will profile virtually every {@link MutableResource}. + * Profiling may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few resources are suspect, you can also call {@link Profiling#profile} + * directly on just those resource, rather than profiling every resource. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + static void profileAllResources() { + MutableResourceFlags.PROFILE_GET_DYNAMICS = true; + detectBusyCells(); + } } /** @@ -102,4 +128,5 @@ static void detectBusyCells() { */ final class MutableResourceFlags { public static boolean DETECT_BUSY_CELLS = false; + public static boolean PROFILE_GET_DYNAMICS = false; } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java index 2f32a88d07..b0b3a1f929 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java @@ -1,8 +1,31 @@ package gov.nasa.jpl.aerie.contrib.streamline.core; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; + /** * A function returning a fully-wrapped dynamics, * and the primary way models track state and report results. */ public interface Resource extends ThinResource>> { + /** + * Turn on profiling for all resources derived through {@link ResourceMonad} + * or created by {@link MutableResource#resource}. + * + *

+ * Calling this method once before constructing your model will profile virtually every resource. + * Profiling may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few resources are suspect, you can also call {@link Profiling#profile} + * directly on just those resource, rather than profiling every resource. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + static void profileAllResources() { + ResourceMonad.profileAllResources(); + MutableResource.profileAllResources(); + } } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java index 04853d3c12..72dff71e60 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; import gov.nasa.jpl.aerie.contrib.streamline.core.ThinResource; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; import gov.nasa.jpl.aerie.contrib.streamline.utils.*; import org.apache.commons.lang3.function.TriFunction; @@ -11,6 +12,7 @@ import java.util.function.Function; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profile; import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; /** @@ -21,14 +23,37 @@ public final class ResourceMonad { private ResourceMonad() {} + private static boolean profileAllResources = false; + /** + * Turn on profiling for all getDynamics calls on {@link Resource}s derived through {@link ResourceMonad}. + * + *

+ * Calling this method once before constructing your model will profile getDynamics on every derived resource. + * Profiling may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few cells are suspect, you can also call {@link Profiling#profile} + * directly on just those resource, rather than profiling every resource. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + public static void profileAllResources() { + profileAllResources = true; + } + public static Resource pure(A a) { - return ThinResourceMonad.pure(DynamicsMonad.pure(a))::getDynamics; + Resource result = ThinResourceMonad.pure(DynamicsMonad.pure(a))::getDynamics; + if (profileAllResources) result = profile(result); + return result; } public static Resource apply(Resource a, Resource> f) { Resource result = ThinResourceMonad.apply(a, ThinResourceMonad.map(f, DynamicsMonad::apply))::getDynamics; addDependency(result, a); addDependency(result, f); + if (profileAllResources) result = profile(result); return result; } @@ -43,6 +68,7 @@ public static Resource join(Resource> a) { // The ::getDynamics at the end up-converts back to Resource, from ThinResource Resource result = ThinResourceMonad.map(ThinResourceMonad.join(ThinResourceMonad.map(a$, ResourceMonad::distribute)), DynamicsMonad::join)::getDynamics; addDependency(result, a); + if (profileAllResources) result = profile(result); return result; } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java index c161cccbb8..575f7688c3 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java @@ -4,6 +4,7 @@ import java.lang.ref.WeakReference; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -25,9 +26,19 @@ public final class Naming { private Naming() {} // Use a WeakHashMap so that naming a thing doesn't prevent it from being garbage-collected. - private static final WeakHashMap>> NAMES = new WeakHashMap<>(); - // Way to inject a temporary "anonymous" name, so derived names still work even when not all args are named. - private static final MutableObject> anonymousName = new MutableObject<>(Optional.empty()); + private static final WeakHashMap>> NAMES = new WeakHashMap<>(); + + private record NamingContext(Set visited, Optional anonymousName) { + NamingContext visit(Object thing) { + var newVisited = new HashSet<>(visited); + newVisited.add(thing); + return new NamingContext(newVisited, anonymousName); + } + + public NamingContext(String anonymousName) { + this(Set.of(), Optional.ofNullable(anonymousName)); + } + } /** * Register a name for thing, as a function of args' names. @@ -36,15 +47,12 @@ private Naming() {} public static T name(T thing, String nameFormat, Object... args) { // Only capture weak references to arguments, so we don't leak memory var args$ = Arrays.stream(args).map(WeakReference::new).toArray(WeakReference[]::new); - NAMES.put(thing, () -> { + NAMES.put(thing, context -> { Object[] argNames = new Object[args$.length]; for (int i = 0; i < args$.length; ++i) { - // Try to resolve the argument name by first looking up and using its registered name, - // or by falling back to the anonymous name. var argName$ = Optional.ofNullable(args$[i].get()) - .flatMap(Naming::getName) - .or(anonymousName::getValue); - if (argName$.isEmpty()) return Optional.empty(); + .flatMap(argRef -> getName(argRef, context)); + if (argName$.isEmpty()) return context.anonymousName(); argNames[i] = argName$.get(); } return Optional.of(nameFormat.formatted(argNames)); @@ -58,7 +66,7 @@ public static T name(T thing, String nameFormat, Object... args) { * returns empty. */ public static Optional getName(Object thing) { - return Optional.ofNullable(NAMES.get(thing)).flatMap(Supplier::get).or(anonymousName::getValue); + return getName(thing, new NamingContext(null)); } /** @@ -66,11 +74,14 @@ public static Optional getName(Object thing) { * Use anonymousName for anything without a name instead of returning empty. */ public static String getName(Object thing, String anonymousName) { - Naming.anonymousName.setValue(Optional.of(anonymousName)); - var result = getName(thing); - Naming.anonymousName.setValue(Optional.empty()); - // This will never throw, because anonymous name will guarantee that some name is found. - return result.orElseThrow(); + // This expression never throws, because context always has a name available. + return getName(thing, new NamingContext(anonymousName)).orElseThrow(); + } + + private static Optional getName(Object thing, NamingContext context) { + return context.visited.contains(thing) + ? context.anonymousName + : NAMES.getOrDefault(thing, NamingContext::anonymousName).apply(context.visit(thing)); } public static String argsFormat(Collection collection) { diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java index 4a369b8322..78ee47592b 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java @@ -12,6 +12,7 @@ import java.util.function.Supplier; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static java.lang.Math.*; import static java.util.Comparator.comparingLong; /** @@ -62,13 +63,15 @@ public static Resource profile(Resource resource) { public static Resource profile(String name, Resource resource) { Resource result = new Resource<>() { + private final Supplier name$ = computeName(name, this); + @Override public ErrorCatching> getDynamics() { - return resourceSamples.computeIfAbsent(computeName(name, this), k -> new CallStats()) + return resourceSamples.computeIfAbsent(name$.get(), k -> new CallStats()) .accrue(resource::getDynamics); } }; - assignName(result, name, resource); + assignName("Resource", result, name, resource); return result; } @@ -78,6 +81,8 @@ public static > MutableResource profile(MutableResou public static > MutableResource profile(String name, MutableResource resource) { MutableResource result = new MutableResource<>() { + private final Supplier name$ = computeName(name, this); + @Override public void emit(DynamicsEffect effect) { resource.emit(effect); @@ -85,11 +90,11 @@ public void emit(DynamicsEffect effect) { @Override public ErrorCatching> getDynamics() { - return resourceSamples.computeIfAbsent(computeName(name, this), k -> new CallStats()) + return resourceSamples.computeIfAbsent(name$.get(), k -> new CallStats()) .accrue(resource::getDynamics); } }; - assignName(result, name, resource); + assignName("MutableResource", result, name, resource); return result; } @@ -99,12 +104,14 @@ public static Condition profile(Condition condition) { public static Condition profile(String name, Condition condition) { Condition result = new Condition() { + private final Supplier name$ = computeName(name, this); + @Override public Optional nextSatisfied(boolean positive, Duration atEarliest, Duration atLatest) { - return accrue(conditionEvaluations, computeName(name, this), () -> condition.nextSatisfied(positive, atEarliest, atLatest)); + return accrue(conditionEvaluations, name$.get(), () -> condition.nextSatisfied(positive, atEarliest, atLatest)); } }; - assignName(result, name, condition); + assignName("Condition", result, name, condition); return result; } @@ -130,32 +137,22 @@ public static Supplier profileTask(Supplier task) { public static Supplier profileTask(String name, Supplier task) { Supplier result = new Supplier<>() { + private final Supplier name$ = computeName(name, this); + @Override public R get() { - return accrue(taskExecutions, computeName(name, this), task); + return accrue(taskExecutions, name$.get(), task); } }; - assignName(result, name, task); + assignName("Task", result, name, task); return result; } - private static long ANONYMOUS_CELL_RESOURCE_ID = 0; public static > MutableResource profileEffects(MutableResource resource) { - return new MutableResource<>() { - private String name = null; + MutableResource result = new MutableResource<>() { @Override public void emit(DynamicsEffect effect) { - // Get the name the first time an effect is emitted, - // which will be after any registrations happen. - if (name == null) { - name = getName(this, "..."); - if (name.equals("...")) { - var generatedName = "CellResource" + (ANONYMOUS_CELL_RESOURCE_ID++); - name(this, generatedName); - name = generatedName; - } - } - resource.emit(x -> accrue(effectsEmitted, name, () -> effect.apply(x))); + resource.emit(x -> accrue(effectsEmitted, getName(this, "..."), () -> effect.apply(x))); } @Override @@ -163,15 +160,20 @@ public ErrorCatching> getDynamics() { return resource.getDynamics(); } }; + assignName("MutableResource", result, null, resource); + return result; } - private static String computeName(String explicitName, Object profiledThing) { - return explicitName != null ? explicitName : getName(profiledThing, "..."); + private static Supplier computeName(String explicitName, Object profiledThing) { + return explicitName != null + ? () -> explicitName + : () -> getName(profiledThing, "..."); } - private static void assignName(Object profiledThing, String explicitName, Object originalThing) { + private static long ANONYMOUS_ID = 0; + private static void assignName(String typeName, Object profiledThing, String explicitName, Object originalThing) { if (explicitName == null) { - name(profiledThing, "%s", originalThing); + name(profiledThing, typeName + (ANONYMOUS_ID++) + " = %s", originalThing); } else { name(profiledThing, explicitName); } @@ -206,12 +208,13 @@ public static void dump() { } } + private static final int MAX_NAME_LENGTH = 60; private static void dumpSampleMap(Map map, long overallElapsedNanos, Comparator sortBy) { - final var nameLength = Math.max(5, map.keySet().stream().mapToInt(String::length).max().orElse(1)); + final var nameLength = min(MAX_NAME_LENGTH, max(5, map.keySet().stream().mapToInt(String::length).max().orElse(1))); final var totalCalls = map.values().stream().mapToLong(c1 -> c1.callsMade).sum(); final var totalNanos = map.values().stream().mapToLong(c1 -> c1.ownNanos).sum(); - final var callsLength = Math.max(5, String.valueOf(totalCalls).length()); - final var millisLength = Math.max(7, String.valueOf(totalNanos / 1_000_000).length()); + final var callsLength = max(5, String.valueOf(totalCalls).length()); + final var millisLength = max(7, String.valueOf(totalNanos / 1_000_000).length()); final var titleFormat = " %-" + nameLength + "s |" + " %" + callsLength + "s %7s |" @@ -255,7 +258,7 @@ private static void dumpSampleMap(Map map, long overallElapse var stats = entry.getValue(); System.out.printf( lineFormat, - entry.getKey(), + fit(entry.getKey(), nameLength), stats.callsMade, 100.0 * stats.callsMade / totalCalls, stats.totalNanos / 1_000_000, @@ -267,6 +270,13 @@ private static void dumpSampleMap(Map map, long overallElapse }); } + private static final String TRUNCATED_INDICATOR = " ..."; + private static String fit(String s, int maxNameLength) { + return s.length() <= maxNameLength + ? s + : s.substring(0, maxNameLength - TRUNCATED_INDICATOR.length()) + TRUNCATED_INDICATOR; + } + private static final class CallStats { public long callsMade = 0; public long totalNanos = 0; diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java index fbdf8a0a7a..0843605ebe 100644 --- a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java @@ -7,6 +7,9 @@ public final class Configuration { @Parameter public boolean traceResources = false; + @Parameter + public boolean profileResources = false; + @Parameter public double approximationTolerance = 1e-2; diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java index 2021ed3c57..ece739efba 100644 --- a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.streamline_demo; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; import gov.nasa.jpl.aerie.merlin.framework.ModelActions; @@ -12,6 +13,7 @@ public final class Mission { public Mission(final gov.nasa.jpl.aerie.merlin.framework.Registrar registrar$, final Configuration config) { var registrar = new Registrar(registrar$, Registrar.ErrorBehavior.Log); if (config.traceResources) registrar.setTrace(); + if (config.profileResources) Resource.profileAllResources(); dataModel = new DataModel(registrar, config); errorTestingModel = new ErrorTestingModel(registrar, config); approximationModel = new ApproximationModel(registrar, config);