All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.deephaven.engine.updategraph.impl.BaseUpdateGraph Maven / Gradle / Ivy

There is a newer version: 0.37.1
Show newest version
/**
 * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
 */

package io.deephaven.engine.updategraph.impl;

import io.deephaven.base.log.LogOutput;
import io.deephaven.base.log.LogOutputAppendable;
import io.deephaven.base.reference.SimpleReference;
import io.deephaven.base.verify.Assert;
import io.deephaven.configuration.Configuration;
import io.deephaven.engine.liveness.LivenessManager;
import io.deephaven.engine.liveness.LivenessScope;
import io.deephaven.engine.liveness.LivenessScopeStack;
import io.deephaven.engine.table.impl.perf.PerformanceEntry;
import io.deephaven.engine.table.impl.perf.UpdatePerformanceTracker;
import io.deephaven.engine.table.impl.util.StepUpdater;
import io.deephaven.engine.updategraph.*;
import io.deephaven.engine.util.reference.CleanupReferenceProcessorInstance;
import io.deephaven.hash.KeyedObjectHashMap;
import io.deephaven.hash.KeyedObjectKey;
import io.deephaven.hotspot.JvmIntrospectionContext;
import io.deephaven.io.log.LogEntry;
import io.deephaven.io.log.impl.LogOutputStringImpl;
import io.deephaven.io.logger.Logger;
import io.deephaven.util.SafeCloseable;
import io.deephaven.util.annotations.TestUseOnly;
import io.deephaven.util.datastructures.SimpleReferenceManager;
import io.deephaven.util.datastructures.linked.IntrusiveDoublyLinkedNode;
import io.deephaven.util.datastructures.linked.IntrusiveDoublyLinkedQueue;
import io.deephaven.util.locks.AwareFunctionalLock;
import io.deephaven.util.process.ProcessEnvironment;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * The BaseUpdateGraph contains common code for other UpdateGraph implementations and a map of named UpdateGraph
 * instances.
 */
public abstract class BaseUpdateGraph implements UpdateGraph, LogOutputAppendable {
    public static final String DEFAULT_UPDATE_GRAPH_NAME = "DEFAULT";

    /**
     * If the provided update graph is a {@link BaseUpdateGraph} then create a PerformanceEntry using the given
     * description. Otherwise, return null.
     *
     * @param updateGraph The update graph to create a performance entry for.
     * @param description The description for the performance entry.
     * @return The performance entry, or null if the update graph is not a {@link BaseUpdateGraph}.
     */
    @Nullable
    public static PerformanceEntry createUpdatePerformanceEntry(
            @Nullable final UpdateGraph updateGraph,
            @Nullable final String description) {
        if (updateGraph instanceof BaseUpdateGraph) {
            final BaseUpdateGraph bug = (BaseUpdateGraph) updateGraph;
            if (bug.updatePerformanceTracker != null) {
                return bug.updatePerformanceTracker.getEntry(description);
            }
            throw new IllegalStateException("Cannot create a performance entry for a BaseUpdateGraph that has "
                    + "not been completely constructed.");
        }
        return null;
    }

    private static final KeyedObjectHashMap INSTANCES = new KeyedObjectHashMap<>(
            new KeyedObjectKey.BasicAdapter<>(UpdateGraph::getName));

    private final Logger log;

    /**
     * Update sources that are part of this BaseUpdateGraph.
     */
    private final SimpleReferenceManager sources =
            new SimpleReferenceManager<>(UpdateSourceRefreshNotification::new);

    /**
     * Recorder for updates source satisfaction as a phase of notification processing.
     */
    private volatile long sourcesLastSatisfiedStep;

    /**
     * The queue of non-terminal notifications to process.
     */
    final IntrusiveDoublyLinkedQueue pendingNormalNotifications =
            new IntrusiveDoublyLinkedQueue<>(IntrusiveDoublyLinkedNode.Adapter.getInstance());

    /**
     * The queue of terminal notifications to process.
     */
    final IntrusiveDoublyLinkedQueue terminalNotifications =
            new IntrusiveDoublyLinkedQueue<>(IntrusiveDoublyLinkedNode.Adapter.getInstance());

    volatile boolean running = true;

    public static final String MINIMUM_CYCLE_DURATION_TO_LOG_MILLIS_PROP =
            "UpdateGraph.minimumCycleDurationToLogMillis";
    public static final long DEFAULT_MINIMUM_CYCLE_DURATION_TO_LOG_NANOSECONDS = TimeUnit.MILLISECONDS.toNanos(
            Configuration.getInstance().getIntegerWithDefault(MINIMUM_CYCLE_DURATION_TO_LOG_MILLIS_PROP, 25));
    private final long minimumCycleDurationToLogNanos;

    /** when to next flush the performance tracker; initializes to zero to force a flush on start */
    private long nextUpdatePerformanceTrackerFlushTimeNanos;

    /**
     * How many cycles we have not logged, but were non-zero.
     */
    long suppressedCycles;
    long suppressedCyclesTotalNanos;
    long suppressedCyclesTotalSafePointTimeMillis;

    /**
     * Accumulated UpdateGraph exclusive lock waits for the current cycle (or previous, if idle).
     */
    private long currentCycleLockWaitTotalNanos;

    public static class AccumulatedCycleStats {
        /**
         * Number of cycles run.
         */
        public int cycles = 0;
        /**
         * Number of cycles run not exceeding their time budget.
         */
        public int cyclesOnBudget = 0;
        /**
         * Accumulated safepoints over all cycles.
         */
        public int safePoints = 0;
        /**
         * Accumulated safepoint time over all cycles.
         */
        public long safePointPauseTimeMillis = 0L;

        public int[] cycleTimesMicros = new int[32];
        public static final int MAX_DOUBLING_LEN = 1024;

        synchronized void accumulate(
                final boolean onBudget,
                final long cycleTimeNanos,
                final long safePoints,
                final long safePointPauseTimeMillis) {
            if (onBudget) {
                ++cyclesOnBudget;
            }
            this.safePoints += safePoints;
            this.safePointPauseTimeMillis += safePointPauseTimeMillis;
            if (cycles >= cycleTimesMicros.length) {
                final int newLen;
                if (cycleTimesMicros.length < MAX_DOUBLING_LEN) {
                    newLen = cycleTimesMicros.length * 2;
                } else {
                    newLen = cycleTimesMicros.length + MAX_DOUBLING_LEN;
                }
                cycleTimesMicros = Arrays.copyOf(cycleTimesMicros, newLen);
            }
            cycleTimesMicros[cycles] = (int) ((cycleTimeNanos + 500) / 1_000);
            ++cycles;
        }

        public synchronized void take(final AccumulatedCycleStats out) {
            out.cycles = cycles;
            out.cyclesOnBudget = cyclesOnBudget;
            out.safePoints = safePoints;
            out.safePointPauseTimeMillis = safePointPauseTimeMillis;
            if (out.cycleTimesMicros.length < cycleTimesMicros.length) {
                out.cycleTimesMicros = new int[cycleTimesMicros.length];
            }
            System.arraycopy(cycleTimesMicros, 0, out.cycleTimesMicros, 0, cycles);
            cycles = 0;
            cyclesOnBudget = 0;
            safePoints = 0;
            safePointPauseTimeMillis = 0;
        }
    }

    public final AccumulatedCycleStats accumulatedCycleStats = new AccumulatedCycleStats();

    /**
     * Abstracts away the processing of non-terminal notifications.
     */
    NotificationProcessor notificationProcessor;

    /**
     * Facilitate GC Introspection during refresh cycles.
     */
    private final JvmIntrospectionContext jvmIntrospectionContext;

    /**
     * The {@link LivenessScope} that should be on top of the {@link LivenessScopeStack} for all run and notification
     * processing. Only non-null while some thread is in {@link #doRefresh(Runnable)}.
     */
    volatile LivenessScope refreshScope;

    /**
     * Is this one of the threads engaged in notification processing? (Either the solitary run thread, or one of the
     * pooled threads it uses in some configurations)
     */
    final ThreadLocal isUpdateThread = ThreadLocal.withInitial(() -> false);

    private final ThreadLocal serialTableOperationsSafe = ThreadLocal.withInitial(() -> false);

    final LogicalClockImpl logicalClock = new LogicalClockImpl();

    /**
     * Encapsulates locking support.
     */
    private final UpdateGraphLock lock;

    /**
     * When UpdateGraph.printDependencyInformation is set to true, the UpdateGraph will print debug information for each
     * notification that has dependency information; as well as which notifications have been completed and are
     * outstanding.
     */
    private final boolean printDependencyInformation =
            Configuration.getInstance().getBooleanWithDefault("UpdateGraph.printDependencyInformation", false);

    private final String name;

    final UpdatePerformanceTracker updatePerformanceTracker;

    /**
     * The BaseUpdateGraph is an abstract class that is suitable for extension by UpdateGraphs that process a set of
     * sources and then the resulting {@link io.deephaven.engine.updategraph.NotificationQueue.Notification
     * Notifications} using a {@link NotificationProcessor}.
     * 
     * @param name the name of the update graph, which must be unique
     * @param allowUnitTestMode is unit test mode allowed, used for configuring the lock
     * @param log the logger for this update graph
     * @param minimumCycleDurationToLogNanos the minimum cycle time, in nanoseconds, that results in cycle times being
     *        logged to at an INFO level
     */
    public BaseUpdateGraph(
            final String name,
            final boolean allowUnitTestMode,
            final Logger log,
            long minimumCycleDurationToLogNanos) {
        this.name = name;
        this.log = log;
        this.minimumCycleDurationToLogNanos = minimumCycleDurationToLogNanos;
        notificationProcessor = PoisonedNotificationProcessor.INSTANCE;
        jvmIntrospectionContext = new JvmIntrospectionContext();
        lock = UpdateGraphLock.create(this, allowUnitTestMode);
        updatePerformanceTracker = new UpdatePerformanceTracker(this);
    }

    public String getName() {
        return name;
    }

    public UpdateGraph getUpdateGraph() {
        return this;
    }

    @Override
    public String toString() {
        return new LogOutputStringImpl().append(this).toString();
    }

    @Override
    public LogicalClock clock() {
        return logicalClock;
    }
    // region Accessors for the shared and exclusive locks

    /**
     * 

* Get the shared lock for this {@link UpdateGraph}. *

* Using this lock will prevent run processing from proceeding concurrently, but will allow other read-only * processing to proceed. *

* The shared lock implementation is expected to support reentrance. *

* This lock does not support {@link java.util.concurrent.locks.Lock#newCondition()}. Use the exclusive * lock if you need to wait on events that are driven by run processing. * * @return The shared lock for this {@link UpdateGraph} */ public AwareFunctionalLock sharedLock() { return lock.sharedLock(); } /** *

* Get the exclusive lock for this {@link UpdateGraph}. *

* Using this lock will prevent run or read-only processing from proceeding concurrently. *

* The exclusive lock implementation is expected to support reentrance. *

* Note that using the exclusive lock while the shared lock is held by the current thread will result in exceptions, * as lock upgrade is not supported. *

* This lock does support {@link java.util.concurrent.locks.Lock#newCondition()}. * * @return The exclusive lock for this {@link UpdateGraph} */ public AwareFunctionalLock exclusiveLock() { return lock.exclusiveLock(); } // endregion Accessors for the shared and exclusive locks /** * Test if this thread is part of our run thread executor service. * * @return whether this is one of our run threads. */ @Override public boolean currentThreadProcessesUpdates() { return isUpdateThread.get(); } @Override public boolean serialTableOperationsSafe() { return serialTableOperationsSafe.get(); } @Override public boolean setSerialTableOperationsSafe(final boolean newValue) { final boolean old = serialTableOperationsSafe.get(); serialTableOperationsSafe.set(newValue); return old; } /** * Add a table to the list of tables to run and mark it as {@link DynamicNode#setRefreshing(boolean) refreshing} if * it was a {@link DynamicNode}. * * @param updateSource The table to be added to the run list */ @Override public void addSource(@NotNull final Runnable updateSource) { if (!running) { throw new IllegalStateException("UpdateGraph is no longer running"); } if (updateSource instanceof DynamicNode) { ((DynamicNode) updateSource).setRefreshing(true); } sources.add(updateSource); } @Override public void removeSource(@NotNull final Runnable updateSource) { sources.remove(updateSource); } /** * Remove a collection of sources from the list of refreshing sources. * * @implNote This will not set the sources as {@link DynamicNode#setRefreshing(boolean) non-refreshing}. * @param sourcesToRemove The sources to remove from the list of refreshing sources */ public void removeSources(final Collection sourcesToRemove) { sources.removeAll(sourcesToRemove); } /** * Return the number of valid sources. * * @return the number of valid sources */ public int sourceCount() { return sources.size(); } /** * Enqueue a notification to be flushed according to its priority. Non-terminal notifications should only be * enqueued during the updating phase of a cycle. That is, they should be enqueued from an update source or * subsequent notification delivery. * * @param notification The notification to enqueue * @see NotificationQueue.Notification#isTerminal() * @see LogicalClock.State */ @Override public void addNotification(@NotNull final Notification notification) { if (notification.isTerminal()) { synchronized (terminalNotifications) { terminalNotifications.offer(notification); } } else { logDependencies().append(Thread.currentThread().getName()).append(": Adding notification ") .append(notification).endl(); synchronized (pendingNormalNotifications) { Assert.eq(logicalClock.currentState(), "logicalClock.currentState()", LogicalClock.State.Updating, "LogicalClock.State.Updating"); pendingNormalNotifications.offer(notification); } notificationProcessor.onNotificationAdded(); } } @Override public boolean maybeAddNotification(@NotNull final Notification notification, final long deliveryStep) { if (notification.isTerminal()) { throw new IllegalArgumentException("Notification must not be terminal"); } logDependencies().append(Thread.currentThread().getName()).append(": Adding notification ").append(notification) .append(" if step is ").append(deliveryStep).endl(); final boolean added; synchronized (pendingNormalNotifications) { // Note that the clock is advanced to idle under the pendingNormalNotifications lock, after which point no // further normal notifications will be processed on this cycle. final long logicalClockValue = logicalClock.currentValue(); if (LogicalClock.getState(logicalClockValue) == LogicalClock.State.Updating && LogicalClock.getStep(logicalClockValue) == deliveryStep) { pendingNormalNotifications.offer(notification); added = true; } else { added = false; } } if (added) { notificationProcessor.onNotificationAdded(); } return added; } @Override public boolean satisfied(final long step) { StepUpdater.checkForOlderStep(step, sourcesLastSatisfiedStep); return sourcesLastSatisfiedStep == step; } /** * Enqueue a collection of notifications to be flushed. * * @param notifications The notification to enqueue * * @see #addNotification(Notification) */ @Override public void addNotifications(@NotNull final Collection notifications) { synchronized (pendingNormalNotifications) { synchronized (terminalNotifications) { notifications.forEach(this::addNotification); } } } /** * @return Whether this UpdateGraph has a mechanism that supports refreshing */ @Override public boolean supportsRefreshing() { return true; } /** * Reset state at the beginning or end of a unit test. * * @param after if this is done after a test, in which case the liveness scope is popped * @param errors the list of errors generated during reset */ @TestUseOnly void resetForUnitTests(final boolean after, final List errors) { sources.clear(); notificationProcessor.shutdown(); synchronized (pendingNormalNotifications) { pendingNormalNotifications.clear(); } isUpdateThread.remove(); synchronized (terminalNotifications) { terminalNotifications.clear(); } logicalClock.resetForUnitTests(); sourcesLastSatisfiedStep = logicalClock.currentStep(); refreshScope = null; if (after) { LivenessManager stackTop; while ((stackTop = LivenessScopeStack.peek()) instanceof LivenessScope) { LivenessScopeStack.pop((LivenessScope) stackTop); } CleanupReferenceProcessorInstance.resetAllForUnitTests(); } ensureUnlocked("unit test reset thread", errors); } @TestUseOnly void resetLock() { lock.reset(); } /** * Flush all non-terminal notifications, complete the logical clock update cycle, then flush all terminal * notifications. * * @param check whether to check that update sources have not yet been satisfied (false in unit test mode) */ void flushNotificationsAndCompleteCycle(boolean check) { // We cannot proceed with normal notifications, nor are we satisfied, until all update source refresh // notifications have been processed. Note that non-update source notifications that require dependency // satisfaction are delivered first to the pendingNormalNotifications queue, and hence will not be processed // until we advance to the flush* methods. // TODO: If and when we properly integrate update sources into the dependency tracking system, we can // discontinue this distinct phase, along with the requirement to treat the UpdateGraph itself as a Dependency. // Until then, we must delay the beginning of "normal" notification processing until all update sources are // done. See IDS-8039. notificationProcessor.doAllWork(); updateSourcesLastSatisfiedStep(check); flushNormalNotificationsAndCompleteCycle(); flushTerminalNotifications(); synchronized (pendingNormalNotifications) { Assert.assertion(pendingNormalNotifications.isEmpty(), "pendingNormalNotifications.isEmpty()"); } } void updateSourcesLastSatisfiedStep(boolean check) { if (check && sourcesLastSatisfiedStep >= logicalClock.currentStep()) { throw new IllegalStateException("Already marked sources as satisfied!"); } sourcesLastSatisfiedStep = logicalClock.currentStep(); } /** * Flush all non-terminal {@link Notification notifications} from the queue. */ private void flushNormalNotificationsAndCompleteCycle() { final IntrusiveDoublyLinkedQueue pendingToEvaluate = new IntrusiveDoublyLinkedQueue<>(IntrusiveDoublyLinkedNode.Adapter.getInstance()); while (true) { final int outstandingCountAtStart = notificationProcessor.outstandingNotificationsCount(); notificationProcessor.beforeNotificationsDrained(); synchronized (pendingNormalNotifications) { pendingToEvaluate.transferAfterTailFrom(pendingNormalNotifications); if (outstandingCountAtStart == 0 && pendingToEvaluate.isEmpty()) { // We complete the cycle here before releasing the lock on pendingNotifications, so that // maybeAddNotification can detect scenarios where the notification cannot be delivered on the // desired step. logicalClock.completeUpdateCycle(); break; } } logDependencies().append(Thread.currentThread().getName()) .append(": Notification queue size=").append(pendingToEvaluate.size()) .append(", outstanding=").append(outstandingCountAtStart) .endl(); boolean nothingBecameSatisfied = true; for (final Iterator it = pendingToEvaluate.iterator(); it.hasNext();) { final Notification notification = it.next(); Assert.eqFalse(notification.isTerminal(), "notification.isTerminal()"); Assert.eqFalse(notification.mustExecuteWithUpdateGraphLock(), "notification.mustExecuteWithUpdateGraphLock()"); final boolean satisfied = notification.canExecute(sourcesLastSatisfiedStep); if (satisfied) { nothingBecameSatisfied = false; it.remove(); logDependencies().append(Thread.currentThread().getName()) .append(": Submitting to notification processor ").append(notification).endl(); notificationProcessor.submit(notification); } else { logDependencies().append(Thread.currentThread().getName()).append(": Unmet dependencies for ") .append(notification).endl(); } } if (outstandingCountAtStart == 0 && nothingBecameSatisfied) { if (!printDependencyInformation) { // Let's drop some breadcrumbs here, because this is a very bad state to be in and hard to debug. log.error().append(Thread.currentThread().getName()) .append(": No outstanding notifications, yet notification queue size=") .append(pendingToEvaluate.size()).endl(); for (final Notification notification : pendingToEvaluate) { log.error().append(Thread.currentThread().getName()).append(": Unmet dependencies for ") .append(notification).endl(); } } throw new IllegalStateException( "No outstanding notifications, yet the notification queue is not empty!"); } if (notificationProcessor.outstandingNotificationsCount() > 0) { notificationProcessor.doWork(); } } synchronized (pendingNormalNotifications) { Assert.eqZero(pendingNormalNotifications.size() + pendingToEvaluate.size(), "pendingNormalNotifications.size() + pendingToEvaluate.size()"); } } /** * Flush all {@link Notification#isTerminal() terminal} {@link Notification notifications} from the queue. * * @implNote Any notification that may have been queued while the clock's state is Updating must be invoked during * this cycle's Idle phase. */ private void flushTerminalNotifications() { synchronized (terminalNotifications) { for (final Iterator it = terminalNotifications.iterator(); it.hasNext();) { final Notification notification = it.next(); Assert.assertion(notification.isTerminal(), "notification.isTerminal()"); if (!notification.mustExecuteWithUpdateGraphLock()) { it.remove(); // for the single threaded queue case; this enqueues the notification; // for the executor service case, this causes the notification to be kicked off notificationProcessor.submit(notification); } } } // run the notifications that must be run on this thread while (true) { final Notification notificationForThisThread; synchronized (terminalNotifications) { notificationForThisThread = terminalNotifications.poll(); } if (notificationForThisThread == null) { break; } runNotification(notificationForThisThread); } // We can not proceed until all of the terminal notifications have executed. notificationProcessor.doAllWork(); } /** * Abstract away the details of satisfied notification processing. */ interface NotificationProcessor { /** * Submit a satisfied notification for processing. * * @param notification The notification */ void submit(@NotNull NotificationQueue.Notification notification); /** * Submit a queue of satisfied notification for processing. * * @param notifications The queue of notifications to * {@link IntrusiveDoublyLinkedQueue#transferAfterTailFrom(IntrusiveDoublyLinkedQueue) transfer} from. * Will become empty as a result of successful completion */ void submitAll(@NotNull IntrusiveDoublyLinkedQueue notifications); /** * Query the number of outstanding notifications submitted to this processor. * * @return The number of outstanding notifications */ int outstandingNotificationsCount(); /** *

* Do work (or in the multi-threaded case, wait for some work to have happened). *

* Caller must know that work is outstanding. */ void doWork(); /** * Do all outstanding work. */ void doAllWork(); /** * Shutdown this notification processor (for unit tests). */ void shutdown(); /** * Called after a pending notification is added. */ void onNotificationAdded(); /** * Called before pending notifications are drained. */ void beforeNotificationsDrained(); } void runNotification(@NotNull final Notification notification) { logDependencies().append(Thread.currentThread().getName()).append(": Executing ").append(notification).endl(); final LivenessScope scope; final boolean releaseScopeOnClose; if (notification.isTerminal()) { // Terminal notifications can't create new notifications, so they have no need to participate in a shared // run scope. scope = new LivenessScope(); releaseScopeOnClose = true; } else { // Non-terminal notifications must use a shared run scope. Assert.neqNull(refreshScope, "refreshScope"); scope = refreshScope == LivenessScopeStack.peek() ? null : refreshScope; releaseScopeOnClose = false; } try (final SafeCloseable ignored = scope == null ? null : LivenessScopeStack.open(scope, releaseScopeOnClose)) { notification.run(); logDependencies().append(Thread.currentThread().getName()).append(": Completed ").append(notification) .endl(); } catch (final Exception e) { log.error().append(Thread.currentThread().getName()) .append(": Exception while executing UpdateGraph notification: ").append(notification) .append(": ").append(e).endl(); ProcessEnvironment.getGlobalFatalErrorReporter() .report("Exception while processing UpdateGraph notification", e); } } class QueueNotificationProcessor implements NotificationProcessor { final IntrusiveDoublyLinkedQueue satisfiedNotifications = new IntrusiveDoublyLinkedQueue<>(IntrusiveDoublyLinkedNode.Adapter.getInstance()); @Override public void submit(@NotNull final Notification notification) { satisfiedNotifications.offer(notification); } @Override public void submitAll(@NotNull IntrusiveDoublyLinkedQueue notifications) { satisfiedNotifications.transferAfterTailFrom(notifications); } @Override public int outstandingNotificationsCount() { return satisfiedNotifications.size(); } @Override public void doWork() { Notification satisfiedNotification; while ((satisfiedNotification = satisfiedNotifications.poll()) != null) { runNotification(satisfiedNotification); } } @Override public void doAllWork() { doWork(); } @Override public void shutdown() { satisfiedNotifications.clear(); } @Override public void onNotificationAdded() {} @Override public void beforeNotificationsDrained() {} } static LogEntry appendAsMillisFromNanos(final LogEntry entry, final long nanos) { if (nanos > 0) { return entry.appendDouble(nanos / 1_000_000.0, 3); } return entry.append(0); } /** * Iterate over all monitored tables and run them. */ void refreshTablesAndFlushNotifications() { final long startTimeNanos = System.nanoTime(); currentCycleLockWaitTotalNanos = 0; jvmIntrospectionContext.startSample(); if (sources.isEmpty()) { exclusiveLock().doLocked(this::flushTerminalNotifications); } else { refreshAllTables(); } jvmIntrospectionContext.endSample(); final long cycleTimeNanos = System.nanoTime() - startTimeNanos; computeStatsAndLogCycle(cycleTimeNanos); } private void computeStatsAndLogCycle(final long cycleTimeNanos) { final long safePointPauseTimeMillis = jvmIntrospectionContext.deltaSafePointPausesTimeMillis(); accumulatedCycleStats.accumulate( isCycleOnBudget(cycleTimeNanos), cycleTimeNanos, jvmIntrospectionContext.deltaSafePointPausesCount(), safePointPauseTimeMillis); if (cycleTimeNanos >= minimumCycleDurationToLogNanos) { if (suppressedCycles > 0) { logSuppressedCycles(); } final double cycleTimeMillis = cycleTimeNanos / 1_000_000.0; LogEntry entry = log.info().append(getName()) .append(": Update Graph Processor cycleTime=").appendDouble(cycleTimeMillis, 3); if (jvmIntrospectionContext.hasSafePointData()) { final long safePointSyncTimeMillis = jvmIntrospectionContext.deltaSafePointSyncTimeMillis(); entry = entry .append("ms, safePointTime=") .append(safePointPauseTimeMillis) .append("ms, safePointTimePct="); if (safePointPauseTimeMillis > 0 && cycleTimeMillis > 0.0) { final double safePointTimePct = 100.0 * safePointPauseTimeMillis / cycleTimeMillis; entry = entry.appendDouble(safePointTimePct, 2); } else { entry = entry.append("0"); } entry = entry.append("%, safePointSyncTime=").append(safePointSyncTimeMillis); } entry = entry.append("ms, lockWaitTime="); entry = appendAsMillisFromNanos(entry, currentCycleLockWaitTotalNanos); entry.append("ms").endl(); return; } if (cycleTimeNanos > 0) { ++suppressedCycles; suppressedCyclesTotalNanos += cycleTimeNanos; suppressedCyclesTotalSafePointTimeMillis += safePointPauseTimeMillis; if (suppressedCyclesTotalNanos >= minimumCycleDurationToLogNanos) { logSuppressedCycles(); } } } /** * Is the provided cycle time on budget? * * @param cycleTimeNanos the cycle time, in nanoseconds * * @return true if the cycle time is within the desired budget */ public boolean isCycleOnBudget(long cycleTimeNanos) { return true; } private void logSuppressedCycles() { LogEntry entry = log.info() .append("Minimal Update Graph Processor cycle times: ") .appendDouble((double) (suppressedCyclesTotalNanos) / 1_000_000.0, 3).append("ms / ") .append(suppressedCycles).append(" cycles = ") .appendDouble( (double) suppressedCyclesTotalNanos / (double) suppressedCycles / 1_000_000.0, 3) .append("ms/cycle average)"); if (jvmIntrospectionContext.hasSafePointData()) { entry = entry .append(", safePointTime=") .append(suppressedCyclesTotalSafePointTimeMillis) .append("ms"); } entry.endl(); suppressedCycles = suppressedCyclesTotalNanos = 0; suppressedCyclesTotalSafePointTimeMillis = 0; } void maybeFlushUpdatePerformance(final long nowNanos, final long checkTime) { if (checkTime >= nextUpdatePerformanceTrackerFlushTimeNanos) { nextUpdatePerformanceTrackerFlushTimeNanos = nowNanos + MILLISECONDS.toNanos(UpdatePerformanceTracker.REPORT_INTERVAL_MILLIS); try { updatePerformanceTracker.flush(); } catch (Exception err) { log.error().append("Error flushing UpdatePerformanceTracker: ").append(err).endl(); } } } /** * In unit tests it can be convenient to force the update performance tracker to flush, without waiting for the * complete REPORT_INTERVAL_MILLIS to elapse. */ @TestUseOnly public void resetNextFlushTime() { nextUpdatePerformanceTrackerFlushTimeNanos = 0; } /** * Refresh all the update sources within an {@link LogicalClock update cycle} after the UpdateGraph has been locked. * At the end of the updates all {@link Notification notifications} will be flushed. */ void refreshAllTables() { doRefresh(() -> sources.forEach((final UpdateSourceRefreshNotification updateSourceNotification, final Runnable unused) -> notificationProcessor.submit(updateSourceNotification))); } /** * Perform a run cycle, using {@code refreshFunction} to ensure the desired update sources are refreshed at the * start. * * @param refreshFunction Function to submit one or more {@link UpdateSourceRefreshNotification update source * refresh notifications} to the {@link NotificationProcessor notification processor} or run them directly. */ private void doRefresh(@NotNull final Runnable refreshFunction) { final long lockStartTimeNanos = System.nanoTime(); exclusiveLock().doLocked(() -> { currentCycleLockWaitTotalNanos += System.nanoTime() - lockStartTimeNanos; if (!running) { return; } synchronized (pendingNormalNotifications) { Assert.eqZero(pendingNormalNotifications.size(), "pendingNormalNotifications.size()"); } Assert.eqNull(refreshScope, "refreshScope"); refreshScope = new LivenessScope(); final long updatingCycleValue = logicalClock.startUpdateCycle(); logDependencies().append("Beginning UpdateGraph cycle step=") .append(logicalClock.currentStep()).endl(); try (final SafeCloseable ignored = LivenessScopeStack.open(refreshScope, true)) { refreshFunction.run(); flushNotificationsAndCompleteCycle(true); } finally { logicalClock.ensureUpdateCycleCompleted(updatingCycleValue); refreshScope = null; } logDependencies().append("Completed UpdateGraph cycle step=") .append(logicalClock.currentStep()).endl(); }); } /** * Re-usable class for adapting update sources to {@link Notification}s. */ static final class UpdateSourceRefreshNotification extends AbstractNotification implements SimpleReference { private final WeakReference updateSourceRef; private UpdateSourceRefreshNotification(@NotNull final Runnable updateSource) { super(false); updateSourceRef = new WeakReference<>(updateSource); } @Override public LogOutput append(@NotNull final LogOutput logOutput) { return logOutput.append("UpdateSourceRefreshNotification{").append(System.identityHashCode(this)) .append(", for UpdateSource{").append(System.identityHashCode(get())).append("}}"); } @Override public boolean canExecute(final long step) { return true; } @Override public void run() { final Runnable updateSource = updateSourceRef.get(); if (updateSource == null) { return; } updateSource.run(); } @Override public Runnable get() { // NB: Arguably we should make get() and clear() synchronized. return updateSourceRef.get(); } @Override public void clear() { updateSourceRef.clear(); } } public LogEntry logDependencies() { if (printDependencyInformation) { return log.info(); } else { return LogEntry.NULL; } } /** * Ensure the lock is not held by the current thread. * * @param callerDescription the description of the caller * @param errors an optional list to populate with errors when the lock is held. */ @TestUseOnly void ensureUnlocked(@NotNull final String callerDescription, @Nullable final List errors) { if (exclusiveLock().isHeldByCurrentThread()) { if (errors != null) { errors.add(callerDescription + ": UpdateGraph exclusive lock is still held"); } while (exclusiveLock().isHeldByCurrentThread()) { exclusiveLock().unlock(); } } if (sharedLock().isHeldByCurrentThread()) { if (errors != null) { errors.add(callerDescription + ": UpdateGraph shared lock is still held"); } while (sharedLock().isHeldByCurrentThread()) { sharedLock().unlock(); } } } public void takeAccumulatedCycleStats(AccumulatedCycleStats updateGraphAccumCycleStats) { accumulatedCycleStats.take(updateGraphAccumCycleStats); } public static UpdateGraph getInstance(final String name) { return INSTANCES.get(name); } /** * Remove a named UpdateGraph. * *

* In addition to removing the UpdateGraph from the instances, an attempt is made to {@link #stop()} it. *

* * @param name the name of the UpdateGraph to remove * @return true if the update graph was found */ public static boolean removeInstance(final String name) { final UpdateGraph graph; synchronized (INSTANCES) { graph = INSTANCES.removeKey(name); if (graph == null) { return false; } } graph.stop(); return true; } /** * Builds and caches a new UpdateGraph named {@code name} and provided by {@code construct}. It is an error if there * is already an UpdateGraph with the same name. * * @param name the name of the new update graph * @param construct A {@link Supplier} to construct an UpdateGraph if no update graph with the name already exists. * The Supplier must provide an update graph with the given name. * * @throws IllegalStateException if an UpdateGraph with the provided name already exists */ public static T buildOrThrow(final String name, final Supplier construct) { synchronized (INSTANCES) { if (INSTANCES.containsKey(name)) { throw new IllegalStateException( String.format("UpdateGraph with name %s already exists", name)); } final T newGraph = construct.get(); Assert.equals(newGraph.getName(), "newGraph.getName()", name, "name"); INSTANCES.put(name, newGraph); return newGraph; } } /** * Returns an existing UpdateGraph with the provided {@code name} if one exists, else returns a new named * UpdateGraph supplied by {@code construct}. * * @param construct A {@link Supplier} to construct an UpdateGraph if no update graph with the name already exists. * The Supplier must provide an update graph with the given name. * * @return the UpdateGraph */ public static T existingOrBuild(final String name, Supplier construct) { return INSTANCES.putIfAbsent(name, (nameToInsert) -> { final T newGraph = construct.get(); Assert.equals(newGraph.getName(), "newGraph.getName()", nameToInsert, "name"); return newGraph; }).cast(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy