RoutePlanner.java

package lab.sample;

import lab.sample.comfort.Breaks;
import lab.sample.model.Leg;
import lab.sample.model.Mode;
import lab.sample.model.Trip;
import lab.sample.speed.SpeedModel;
import lab.sample.speed.Speeds;
import lab.sample.terrain.Terrain;
import lab.sample.traffic.Traffic;
import lab.sample.transfer.Connections;
import lab.sample.weather.LuggagePenalty;
import lab.sample.weather.WeatherPenalty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <b>The function under analysis.</b> "How long does it take to get from A to B?"
 *
 * <p>This is the entry point every tool must be able to target: while this function runs,
 * give me the lines executed, the call tree and the parameter values.
 *
 * <p>It calls four families of computation <b>depending on the context</b>, never all at
 * once:
 * <ul>
 *   <li>the chosen mode's speed — always, but through a different implementation;</li>
 *   <li>traffic jams — car at rush hour only;</li>
 *   <li>weather and luggage — bike and walking only;</li>
 *   <li>connections — train only;</li>
 *   <li>breaks — beyond a <em>computed</em> duration, whatever the mode.</li>
 * </ul>
 * Two different trips therefore produce two different call trees, from the same call to the
 * same method. That is precisely what tools are asked to show.
 *
 * <p>The logging is there for a reason that is not functional: {@code org.slf4j} is not in
 * the JDK, so nothing folds it away automatically, and it shows up in the call tree in the
 * middle of the route computation. It is the archetypal library one wants to hide by
 * configuration — see {@code HIDDEN_PACKAGES}.
 */
public final class RoutePlanner {

    private static final Logger log = LoggerFactory.getLogger(RoutePlanner.class);

    private RoutePlanner() {}

    /**
     * @param trip the trip to evaluate
     * @return the total estimated duration, in minutes
     */
    public static double travelTimeMinutes(Trip trip) {
        // A call deliberately left in the hot loop: that is what makes slf4j visible in the
        // profile, hence hideable, hence demonstrable.
        log.debug("evaluating trip {}", trip.id());
        SpeedModel speed = Speeds.forMode(trip.mode());

        double minutes = 0;
        // An indexed loop on purpose: the for-each loop over an immutable List allocated one
        // iterator per leg, which on its own accounted for half the samples. Seen in the
        // profiler, fixed, re-measured.
        for (int i = 0; i < trip.legs().size(); i++) {
            minutes += legMinutes(trip.legs().get(i), speed, trip.mode());
        }

        if (Traffic.applies(trip.mode(), trip.timeOfDay())) {
            minutes += Traffic.delayMinutes(trip.legs());
        }

        if (WeatherPenalty.applies(trip.mode())) {
            minutes *= WeatherPenalty.slowdownFactor(trip.weather());
            minutes *= LuggagePenalty.slowdownFactor(trip.withLuggage());
        }

        if (trip.mode() == Mode.TRAIN) {
            minutes += Connections.waitMinutes(trip.legs(), 0);
        }

        // Decided on the duration obtained, not on an argument received: see Breaks.
        minutes += Breaks.totalMinutes(minutes);

        return minutes;
    }

    private static double legMinutes(Leg leg, SpeedModel speed, Mode mode) {
        double minutes = leg.distanceKm() / speed.kmPerHour(leg) * 60.0;
        if (Terrain.applies(mode)) {
            minutes *= Terrain.slowdownFactor(leg, mode);
        }
        return minutes;
    }
}