@Test
  public void testCycleSimpleGraph() {
    ViewProcessorTestEnvironment env = new ViewProcessorTestEnvironment();
    env.init();
    CompiledViewDefinitionWithGraphsImpl compiledViewDefinition =
        env.compileViewDefinition(Instant.now(), VersionCorrection.LATEST);
    DependencyGraph graph =
        compiledViewDefinition.getDependencyGraph(
            ViewProcessorTestEnvironment.TEST_CALC_CONFIG_NAME);
    DependencyGraph cycledGraph = cycleObject(DependencyGraph.class, graph);

    assertEquals(
        graph.getCalculationConfigurationName(), cycledGraph.getCalculationConfigurationName());
    assertEquals(graph.getAllComputationTargets(), cycledGraph.getAllComputationTargets());
    assertEquals(graph.getOutputSpecifications(), cycledGraph.getOutputSpecifications());
    assertEquals(graph.getSize(), cycledGraph.getSize());
    assertEquals(
        graph.getTerminalOutputSpecifications(), cycledGraph.getTerminalOutputSpecifications());

    for (DependencyNode node : graph.getDependencyNodes()) {
      boolean isRoot = graph.getRootNodes().contains(node);
      for (ValueSpecification spec : node.getOutputValues()) {
        DependencyNode equivalentNode = cycledGraph.getNodeProducing(spec);
        assertEquals(isRoot, cycledGraph.getRootNodes().contains(equivalentNode));
        assertEquals(node.getInputValues(), equivalentNode.getInputValues());
        assertEquals(node.getOutputValues(), equivalentNode.getOutputValues());
        assertEquals(node.getTerminalOutputValues(), equivalentNode.getTerminalOutputValues());
      }
    }
  }
  // -------------------------------------------------------------------------
  private EngineResourceReference<SingleComputationCycle> createCycle(
      ViewCycleExecutionOptions executionOptions,
      CompiledViewDefinitionWithGraphsImpl compiledViewDefinition,
      VersionCorrection versionCorrection) {
    // View definition was compiled based on compilation options, which might have only included an
    // indicative
    // valuation time. A further check ensures that the compiled view definition is still valid.
    if (!compiledViewDefinition.isValidFor(executionOptions.getValuationTime())) {
      throw new OpenGammaRuntimeException(
          "Compiled view definition "
              + compiledViewDefinition
              + " not valid for execution options "
              + executionOptions);
    }
    UniqueId cycleId = getViewProcess().generateCycleId();

    ComputationResultListener streamingResultListener =
        new ComputationResultListener() {
          @Override
          public void resultAvailable(ViewComputationResultModel result) {
            cycleFragmentCompleted(result);
          }
        };
    SingleComputationCycle cycle =
        new SingleComputationCycle(
            cycleId,
            getViewProcess().getUniqueId(),
            streamingResultListener,
            getProcessContext(),
            compiledViewDefinition,
            executionOptions,
            versionCorrection);
    return getCycleManager().manage(cycle);
  }
  @Override
  public void valuesChanged(Collection<ValueRequirement> values) {
    if (!getExecutionOptions()
        .getFlags()
        .contains(ViewExecutionFlags.TRIGGER_CYCLE_ON_MARKET_DATA_CHANGED)) {
      return;
    }

    CompiledViewDefinitionWithGraphsImpl compiledView = getCachedCompiledViewDefinition();
    if (compiledView == null) {
      return;
    }
    // Since this happens for every tick, for every job, we need to use the quick call here
    if (compiledView.hasAnyMarketDataRequirements(values)) {
      marketDataChanged();
    }
  }
  private CompiledViewDefinitionWithGraphsImpl getCompiledViewDefinition(
      Instant valuationTime, VersionCorrection versionCorrection) {
    long functionInitId =
        getProcessContext()
            .getFunctionCompilationService()
            .getFunctionCompilationContext()
            .getFunctionInitId();
    CompiledViewDefinitionWithGraphsImpl compiledViewDefinition;
    updateViewDefinitionIfRequired();
    if (_compilationDirty) {
      _compilationDirty = false;
      invalidateCachedCompiledViewDefinition();
      compiledViewDefinition = null;
    } else {
      compiledViewDefinition = getCachedCompiledViewDefinition();
    }
    if (compiledViewDefinition != null
        && compiledViewDefinition.isValidFor(valuationTime)
        && functionInitId == compiledViewDefinition.getFunctionInitId()) {
      // Existing cached model is valid (an optimisation for the common case of similar, increasing
      // valuation times)
      return compiledViewDefinition;
    }

    try {
      MarketDataAvailabilityProvider availabilityProvider =
          getMarketDataProvider().getAvailabilityProvider();
      ViewCompilationServices compilationServices =
          getProcessContext().asCompilationServices(availabilityProvider);
      compiledViewDefinition =
          ViewDefinitionCompiler.compile(
              _viewDefinition, compilationServices, valuationTime, versionCorrection);

      if (isTerminated()) {
        return compiledViewDefinition; // [PLAT-1904] If we can't terminate the compilation at least
                                       // avoid doing the subscribe etc.
      }
    } catch (Exception e) {
      String message =
          MessageFormat.format(
              "Error compiling view definition {0} for time {1}",
              getViewProcess().getDefinitionId(), valuationTime);
      viewDefinitionCompilationFailed(valuationTime, new OpenGammaRuntimeException(message, e));
      throw new OpenGammaRuntimeException(message, e);
    }
    setCachedCompiledViewDefinition(compiledViewDefinition);
    // [PLAT-984]
    // Assume that valuation times are increasing in real-time towards the expiry of the view
    // definition, so that we
    // can predict the time to expiry. If this assumption is wrong then the worst we do is trigger
    // an unnecessary
    // cycle. In the predicted case, we trigger a cycle on expiry so that any new market data
    // subscriptions are made
    // straight away.
    if (compiledViewDefinition.getValidTo() != null) {
      Duration durationToExpiry =
          getMarketDataProvider()
              .getRealTimeDuration(valuationTime, compiledViewDefinition.getValidTo());
      long expiryNanos = System.nanoTime() + durationToExpiry.toNanosLong();
      _compilationExpiryCycleTrigger.set(expiryNanos, ViewCycleTriggerResult.forceFull());
    } else {
      _compilationExpiryCycleTrigger.reset();
    }

    // Notify the view that a (re)compilation has taken place before going on to do any
    // time-consuming work.
    // This might contain enough for clients to e.g. render an empty grid in which results will
    // later appear.
    viewDefinitionCompiled(compiledViewDefinition);

    // Update the market data subscriptions to whatever is now required, ensuring the computation
    // cycle can find the
    // required input data when it is executed.
    setMarketDataSubscriptions(compiledViewDefinition.getMarketDataRequirements().keySet());
    return compiledViewDefinition;
  }
  /**
   * Determines whether to run, and runs if required, a single computation cycle using the following
   * rules:
   *
   * <ul>
   *   <li>A computation cycle can only be triggered if the relevant minimum computation period has
   *       passed since the start of the previous cycle.
   *   <li>A computation cycle will be forced if the relevant maximum computation period has passed
   *       since the start of the previous cycle.
   *   <li>A full computation is preferred over a delta computation if both are possible.
   *   <li>Performing a full computation also updates the times to the next delta computation; i.e.
   *       a full computation is considered to be as good as a delta.
   * </ul>
   */
  @Override
  protected void runOneCycle() {
    // Exception handling is important here to ensure that computation jobs do not just die quietly
    // while consumers are
    // potentially blocked, waiting for results.

    ViewCycleType cycleType;
    try {
      cycleType = waitForNextCycle();
    } catch (InterruptedException e) {
      return;
    }

    ViewCycleExecutionOptions executionOptions = null;
    try {
      if (!getExecutionOptions().getExecutionSequence().isEmpty()) {
        executionOptions =
            getExecutionOptions()
                .getExecutionSequence()
                .getNext(getExecutionOptions().getDefaultExecutionOptions());
        s_logger.debug("Next cycle execution options: {}", executionOptions);
      }
      if (executionOptions == null) {
        s_logger.info("No more view cycle execution options");
        processCompleted();
        return;
      }
    } catch (Exception e) {
      s_logger.error(
          "Error obtaining next view cycle execution options from sequence for view process "
              + getViewProcess(),
          e);
      return;
    }

    if (executionOptions.getMarketDataSpecification() == null) {
      s_logger.error("No market data specification for cycle");
      cycleExecutionFailed(
          executionOptions,
          new OpenGammaRuntimeException("No market data specification for cycle"));
      return;
    }

    MarketDataSnapshot marketDataSnapshot;
    try {
      if (getMarketDataProvider() == null
          || !getMarketDataProvider().isCompatible(executionOptions.getMarketDataSpecification())) {
        // A different market data provider is required. We support this because we can, but
        // changing provider is not the
        // most efficient operation.
        if (getMarketDataProvider() != null) {
          s_logger.info("Replacing market data provider between cycles");
        }
        replaceMarketDataProvider(executionOptions.getMarketDataSpecification());
      }

      // Obtain the snapshot in case it is needed, but don't explicitly initialise it until the data
      // is required
      marketDataSnapshot =
          getMarketDataProvider().snapshot(executionOptions.getMarketDataSpecification());
    } catch (Exception e) {
      s_logger.error("Error with market data provider", e);
      cycleExecutionFailed(
          executionOptions, new OpenGammaRuntimeException("Error with market data provider", e));
      return;
    }

    Instant compilationValuationTime;
    try {
      if (executionOptions.getValuationTime() != null) {
        compilationValuationTime = executionOptions.getValuationTime();
      } else {
        // Neither the cycle-specific options nor the defaults have overridden the valuation time so
        // use the time
        // associated with the market data snapshot. To avoid initialising the snapshot perhaps
        // before the required
        // inputs are known or even subscribed to, only ask for an indication at the moment.
        compilationValuationTime = marketDataSnapshot.getSnapshotTimeIndication();
        if (compilationValuationTime == null) {
          throw new OpenGammaRuntimeException(
              "Market data snapshot "
                  + marketDataSnapshot
                  + " produced a null indication of snapshot time");
        }
      }
    } catch (Exception e) {
      s_logger.error("Error obtaining compilation valuation time", e);
      cycleExecutionFailed(
          executionOptions,
          new OpenGammaRuntimeException("Error obtaining compilation valuation time", e));
      return;
    }

    VersionCorrection versionCorrection = getResolvedVersionCorrection();
    final CompiledViewDefinitionWithGraphsImpl compiledViewDefinition;
    try {
      compiledViewDefinition =
          getCompiledViewDefinition(compilationValuationTime, versionCorrection);
      if (isTerminated()) {
        return; // [PLAT-1904]
      }
    } catch (Exception e) {
      String message =
          MessageFormat.format(
              "Error obtaining compiled view definition {0} for time {1} at version-correction {2}",
              getViewProcess().getDefinitionId(), compilationValuationTime, versionCorrection);
      s_logger.error(message);
      cycleExecutionFailed(executionOptions, new OpenGammaRuntimeException(message, e));
      return;
    }

    try {
      if (getExecutionOptions().getFlags().contains(ViewExecutionFlags.AWAIT_MARKET_DATA)) {
        marketDataSnapshot.init(
            compiledViewDefinition.getMarketDataRequirements().keySet(),
            MARKET_DATA_TIMEOUT_MILLIS,
            TimeUnit.MILLISECONDS);
      } else {
        marketDataSnapshot.init();
      }
      if (executionOptions.getValuationTime() == null) {
        executionOptions.setValuationTime(marketDataSnapshot.getSnapshotTime());
      }
    } catch (Exception e) {
      s_logger.error("Error initializing snapshot {}", marketDataSnapshot);
      cycleExecutionFailed(
          executionOptions,
          new OpenGammaRuntimeException("Error initializing snapshot" + marketDataSnapshot, e));
    }

    EngineResourceReference<SingleComputationCycle> cycleReference;
    try {
      cycleReference = createCycle(executionOptions, compiledViewDefinition, versionCorrection);
    } catch (Exception e) {
      s_logger.error("Error creating next view cycle for view process " + getViewProcess(), e);
      return;
    }

    if (_executeCycles) {
      try {
        final SingleComputationCycle singleComputationCycle = cycleReference.get();
        final Set<String> configurationNames =
            singleComputationCycle.getAllCalculationConfigurationNames();

        final HashMap<String, Collection<ComputationTarget>> configToComputationTargets =
            new HashMap<String, Collection<ComputationTarget>>();
        for (String configName : configurationNames) {
          DependencyGraph dependencyGraph =
              singleComputationCycle.getExecutableDependencyGraph(configName);
          configToComputationTargets.put(configName, dependencyGraph.getAllComputationTargets());
        }

        final HashMap<String, Map<ValueSpecification, Set<ValueRequirement>>>
            configToTerminalOutputs =
                new HashMap<String, Map<ValueSpecification, Set<ValueRequirement>>>();
        for (String configName : configurationNames) {
          DependencyGraph dependencyGraph =
              singleComputationCycle.getExecutableDependencyGraph(configName);
          configToTerminalOutputs.put(configName, dependencyGraph.getTerminalOutputs());
        }

        cycleInitiated(
            new SimpleCycleInfo(
                marketDataSnapshot.getUniqueId(),
                compiledViewDefinition.getViewDefinition().getUniqueId(),
                versionCorrection,
                executionOptions.getValuationTime(),
                configurationNames,
                configToComputationTargets,
                configToTerminalOutputs));
        executeViewCycle(
            cycleType,
            cycleReference,
            marketDataSnapshot,
            getViewProcess().getCalcJobResultExecutorService());
      } catch (InterruptedException e) {
        // Execution interrupted - don't propagate as failure
        s_logger.info("View cycle execution interrupted for view process {}", getViewProcess());
        cycleReference.release();
        return;
      } catch (Exception e) {
        // Execution failed
        s_logger.error("View cycle execution failed for view process " + getViewProcess(), e);
        cycleReference.release();
        cycleExecutionFailed(executionOptions, e);
        return;
      }
    }

    // Don't push the results through if we've been terminated, since another computation job could
    // be running already
    // and the fact that we've been terminated means the view is no longer interested in the result.
    // Just die quietly.
    if (isTerminated()) {
      cycleReference.release();
      return;
    }

    if (_executeCycles) {
      cycleCompleted(cycleReference.get());
    }

    if (getExecutionOptions().getExecutionSequence().isEmpty()) {
      processCompleted();
    }

    if (_executeCycles) {
      if (_previousCycleReference != null) {
        _previousCycleReference.release();
      }
      _previousCycleReference = cycleReference;
    }
  }