/* Generated By:JJTree: Do not edit this line. OExpansionItem.java Version 4.3 */
/* JavaCCOptions:MULTI=true,NODE_USES_PARSER=false,VISITOR=true,TRACK_TOKENS=true,NODE_PREFIX=O,NODE_EXTENDS=,NODE_FACTORY=,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */
package com.orientechnologies.orient.core.sql.parser;

import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.sql.executor.OResult;
import com.orientechnologies.orient.core.sql.executor.OResultInternal;

import java.util.Map;

public class ONestedProjectionItem extends SimpleNode {

  protected boolean exclude = false;

  protected boolean star = false;

  protected OExpression expression;
  protected boolean rightWildcard = false;

  protected ONestedProjection expansion;
  protected OIdentifier       alias;

  public ONestedProjectionItem(int id) {
    super(id);
  }

  public ONestedProjectionItem(OrientSql p, int id) {
    super(p, id);
  }

  @Override
  public ONestedProjectionItem copy() {
    ONestedProjectionItem result = new ONestedProjectionItem(-1);
    result.exclude = exclude;
    result.star = star;
    result.expression = expression == null ? null : expression.copy();
    result.rightWildcard = rightWildcard;
    result.expansion = expansion == null ? null : expansion.copy();
    result.alias = alias == null ? null : alias.copy();
    return result;
  }

  /**
   * given a property name, calculates if this property name matches this nested projection item, eg.
   *

   *
this is a *, so it matches any property name

   *
the field name for this projection item is the same as the input property name

   *
this item has a wildcard and the partial field is a prefix of the input property name

   *

   *
   * @param propertyName
   *
   * @return
   */
  public boolean matches(String propertyName) {
    if (star) {
      return true;
    }
    if (expression != null) {
      String fieldString = expression.getDefaultAlias().getStringValue();
      if (fieldString.equals(propertyName)) {
        return true;
      }
      if (rightWildcard && propertyName.startsWith(fieldString)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void toString(Map<Object, Object> params, StringBuilder builder) {
    if (exclude) {
      builder.append("!");
    }
    if (star) {
      builder.append("*");
    }
    if (expression != null) {
      expression.toString(params, builder);
      if (rightWildcard) {
        builder.append("*");
      }
    }
    if (expansion != null) {
      expansion.toString(params, builder);
    }
    if (alias != null) {
      builder.append(" AS ");
      alias.toString(params, builder);
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o)
      return true;
    if (o == null || getClass() != o.getClass())
      return false;

    ONestedProjectionItem that = (ONestedProjectionItem) o;

    if (exclude != that.exclude)
      return false;
    if (star != that.star)
      return false;
    if (rightWildcard != that.rightWildcard)
      return false;
    if (expression != null ? !expression.equals(that.expression) : that.expression != null)
      return false;
    if (expansion != null ? !expansion.equals(that.expansion) : that.expansion != null)
      return false;
    return alias != null ? alias.equals(that.alias) : that.alias == null;
  }

  @Override
  public int hashCode() {
    int result = (exclude ? 1 : 0);
    result = 31 * result + (star ? 1 : 0);
    result = 31 * result + (expression != null ? expression.hashCode() : 0);
    result = 31 * result + (rightWildcard ? 1 : 0);
    result = 31 * result + (expansion != null ? expansion.hashCode() : 0);
    result = 31 * result + (alias != null ? alias.hashCode() : 0);
    return result;
  }

  public Object expand(OExpression expression, String name, Object value, OCommandContext ctx, int recursion) {
    return expansion.apply(expression, value, ctx);
  }

  public OResult serialize() {
    OResultInternal result = new OResultInternal();
    result.setProperty("exclude", exclude);
    result.setProperty("star", star);
    if (expression != null) {
      result.setProperty("expression", expression.serialize());
    }
    result.setProperty("rightWildcard", rightWildcard);
    if (expansion != null) {
      result.setProperty("expansion", expansion.serialize());
    }
    if (alias != null) {
      result.setProperty("alias", alias.serialize());
    }
    return result;
  }

  public void deserialize(OResult fromResult) {
    exclude = fromResult.getProperty("exclude");
    star = fromResult.getProperty("star");
    if (fromResult.getProperty("field") != null) {
      expression = new OExpression(-1);
      expression.deserialize(fromResult.getProperty("expression"));
    }
    rightWildcard = fromResult.getProperty("rightWildcard");
    if (fromResult.getProperty("expansion") != null) {
      expansion = new ONestedProjection(-1);
      expansion.deserialize(fromResult.getProperty("expansion"));
    }
    if (fromResult.getProperty("alias") != null) {
      alias = OIdentifier.deserialize(fromResult.getProperty("alias"));
    }
  }
}
/* JavaCC - OriginalChecksum=606b3fe37ff952934e3e2e3daa9915f2 (do not edit this line) */


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.sql;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OSchema;
import com.orientechnologies.orient.core.sql.parser.OIdentifier;
import com.orientechnologies.orient.core.sql.parser.OTruncateClusterStatement;
import com.orientechnologies.orient.core.storage.OCluster;
import com.orientechnologies.orient.core.storage.OStorage;

import java.io.IOException;
import java.util.Map;

/**
 * SQL TRUNCATE CLUSTER command: Truncates an entire record cluster.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OCommandExecutorSQLTruncateCluster extends OCommandExecutorSQLAbstract implements OCommandDistributedReplicateRequest {
  public static final String KEYWORD_TRUNCATE = "TRUNCATE";
  public static final String KEYWORD_CLUSTER  = "CLUSTER";
  private String             clusterName;

  @SuppressWarnings("unchecked")
  public OCommandExecutorSQLTruncateCluster parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

    String queryText = textRequest.getText();
    String originalQuery = queryText;
    try {
      queryText = preParse(queryText, iRequest);
      textRequest.setText(queryText);

      init((OCommandRequestText) iRequest);

      StringBuilder word = new StringBuilder();

      int oldPos = 0;
      int pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_TRUNCATE))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_TRUNCATE + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_CLUSTER))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_CLUSTER + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserText, oldPos, word, true);
      if (pos == -1)
        throw new OCommandSQLParsingException("Expected cluster name. Use " + getSyntax(), parserText, oldPos);

      clusterName = decodeClusterName(word.toString());

      if (preParsedStatement != null) { // new parser, this will be removed and implemented with the new executor
        OIdentifier name = ((OTruncateClusterStatement) preParsedStatement).clusterName;
        if (name != null) {
          clusterName = name.getStringValue();
        }
      }

      final ODatabaseDocument database = getDatabase();
      if (database.getClusterIdByName(clusterName) == -1)
        throw new OCommandSQLParsingException("Cluster '" + clusterName + "' not found", parserText, oldPos);
    } finally {
      textRequest.setText(originalQuery);
    }
    return this;
  }

  private String decodeClusterName(String s) {
    return decodeClassName(s);
  }

  /**
   * Execute the command.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (clusterName == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    final ODatabaseDocumentInternal database = getDatabase();

    final int clusterId = database.getClusterIdByName(clusterName);
    if (clusterId < 0) {
      throw new ODatabaseException("Cluster with name " + clusterName + " does not exist");
    }

    final OSchema schema = database.getMetadata().getSchema();
    final OClass clazz = schema.getClassByClusterId(clusterId);
    if (clazz == null) {
      final OStorage storage = database.getStorage();
      final OCluster cluster = storage.getClusterById(clusterId);

      if (cluster == null) {
        throw new ODatabaseException("Cluster with name " + clusterName + " does not exist");
      }

      try {
        database.checkForClusterPermissions(cluster.getName());
        cluster.truncate();
      } catch (IOException ioe) {
        throw OException.wrapException(new ODatabaseException("Error during truncation of cluster with name " + clusterName), ioe);
      }
    } else {
      clazz.truncateCluster(clusterName);
    }
    return true;
  }

  @Override
  public long getDistributedTimeout() {
    return getDatabase().getConfiguration().getValueAsLong(OGlobalConfiguration.DISTRIBUTED_COMMAND_TASK_SYNCH_TIMEOUT);
  }

  @Override
  public String getSyntax() {
    return "TRUNCATE CLUSTER <cluster-name>";
  }

  @Override
  public QUORUM_TYPE getQuorumType() {
    return QUORUM_TYPE.WRITE;
  }
}


/*
 *
 *  *  Copyright 2016 OrientDB LTD (info(at)orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://www.orientdb.com
 *
 */
package com.orientechnologies.orient.core.storage.impl.local.statistic;

import com.orientechnologies.common.concur.lock.OReadersWriterSpinLock;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.ORawPair;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.storage.OIdentifiableStorage;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.cache.OReadCache;
import com.orientechnologies.orient.core.storage.cache.OWriteCache;
import com.orientechnologies.orient.core.storage.cache.local.OWOWCache;
import com.orientechnologies.orient.core.storage.cache.local.twoq.O2QCache;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;

import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.orientechnologies.orient.core.storage.impl.local.statistic.OSessionStoragePerformanceStatistic.ComponentType;
import static com.orientechnologies.orient.core.storage.impl.local.statistic.OSessionStoragePerformanceStatistic.PerformanceCountersHolder;
import static com.orientechnologies.orient.core.storage.impl.local.statistic.OSessionStoragePerformanceStatistic.PerformanceSnapshot;
import static com.orientechnologies.orient.core.storage.impl.local.statistic.OSessionStoragePerformanceStatistic.StorageCountersHolder;
import static com.orientechnologies.orient.core.storage.impl.local.statistic.OSessionStoragePerformanceStatistic.WALCountersHolder;
import static com.orientechnologies.orient.core.storage.impl.local.statistic.OSessionStoragePerformanceStatistic.WritCacheCountersHolder;

/**
 * Aggregator of performance statistic for whole storage.
 * Allows to gather performance statistic for single thread or for all threads in storage.
 * In case of gathering statistics for single thread statistic for all period is gathered and average values are provided.
 * If you gather statistic for whole system time series of measurements are provided.
 * You can not measure statistic for whole system and for chosen threads at the same time.
 * To gather statistic for single thread use following workflow:
 *

 *
Call {@link #startThreadMonitoring()}

 *
Execute database commands

 *
Call {@link #stopThreadMonitoring()}

 *

 * Instance of {@link OSessionStoragePerformanceStatistic} returned as result of call of last method will contain all
 * performance data are gathered during storage monitoring.
 * To gather statistic for whole system use following workflow:
 *

 *
Call {@link #startMonitoring()}

 *
During monitoring of storage use getXXX methods to get information about performance numbers

 *
At the end of monitoring call {@link #stopMonitoring()}

 *

 * You may access performance data both after you stopped gathering statistic and during gathering of statistic.
 * You may manipulate by manager directly from Java or from JMX from bean with name which consist of prefix {@link #MBEAN_PREFIX}
 * and storage name.
 * If {@link com.orientechnologies.orient.core.storage.impl.local.paginated.base.ODurableComponent} participates in
 * performance monitoring it has to register itself using method {@link #registerComponent(String)}
 */
public class OPerformanceStatisticManager {
  /**
   * Prefix of name of JMX bean.
   */
  private static final String MBEAN_PREFIX = "com.orientechnologies.orient.core.storage.impl.local.statistic:type=OPerformanceStatisticManagerMXBean";

  /*
   * Class consist of three key fields:
   *

   *

   *
{@link #statistics}

   *
{@link #deadThreadsStatistic}

   *
{@link #postMeasurementStatistic}

   *

   *

   * When we call {@link #getSessionPerformanceStatistic()} we put current thread and related {@link OSessionStoragePerformanceStatistic} object
   * into {@link #statistics} map. Because some threads may die and new born during measurement of performance static each call to
   * getXXX method checks whether thread in {@link #statistics} map leave or dead. If dead threads are found they removed from map
   * and all statics from those threads is accumulated in {@link #deadThreadsStatistic} field.
   *

   * At the end of measurement of performance data both fields {@link #statistics} and {@link #deadThreadsStatistic} are cleared and all
   * statistic is accumulated in {@link #postMeasurementStatistic} statistic. This field will be cleared during next call of
   * {@link #startMonitoring()} method.
   */

  /**
   * Interval between time series for each thread statistic.
   *
   * @see OSessionStoragePerformanceStatistic
   */
  private final long intervalBetweenSnapshots;

  /**
   * Interval between two time series.
   */
  private final long cleanUpInterval;

  /**
   * Lock for switching on/off statistic measurements.
   */
  private final OReadersWriterSpinLock switchLock = new OReadersWriterSpinLock();

  /**
   * Indicates whether gathering of performance data for whole system is switched on/off.
   */
  private volatile boolean enabled = false;

  /**
   * Indicates whether gathering of performance data for single thread is switched on/off.
   */
  private final ThreadLocal<Boolean> enabledForCurrentThread = ThreadLocal.withInitial(() -> false);

  /**
   * Map which bounds performance statistic for currently running threads.
   */
  private final ConcurrentHashMap<Thread, OSessionStoragePerformanceStatistic> statistics = new ConcurrentHashMap<>();

  /**
   * Accumulated statistic for threads which were active during early stages of performance measurement and are died now.
   */
  private volatile ImmutableStatistic deadThreadsStatistic;

  /**
   * Statistic accumulated after {@link #stopMonitoring()} method was called.
   */
  private volatile ImmutableStatistic postMeasurementStatistic;

  /**
   * Lock which ensures that only single thread may accumulate statistic from dead threads at the moment.
   */
  private final Lock deadThreadsUpdateLock = new ReentrantLock();

  /**
   * Indicates whether JMX bean for current object is already registered.
   */
  private final AtomicBoolean mbeanIsRegistered = new AtomicBoolean();

  /**
   * List of all components which registered itself as participating in performance monitoring.
   */
  private final List<String> componentNames = new CopyOnWriteArrayList<>();

  /**
   * Amount of full checkpoints are done for current storage.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long fullCheckpointCount = -1;

  /**
   * Size of write ahead log in bytes.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long walSize = -1;

  /**
   * Amount of times when WAL cache is completely filled in and force flush is needed.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long walCacheOverflowCount = -1;

  /**
   * Size of read cache in bytes.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long readCacheSize = -1;

  /**
   * Size of write cache in bytes.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long writeCacheSize = -1;

  /**
   * Size of part of write cache which is not shared with read cache in bytes.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long exclusiveWriteCacheSize = 1;

  /**
   * Amount of times when size of write cache is not enough to keep dirty pages and flush of write cache is forced.
   * Value is updated on demand if monitoring is switched on.
   * Supported only for disk based storage.
   */
  private long writeCacheOverflowCount = -1;

  /**
   * Instance of storage to which performance manager belongs to.
   */
  private final OAbstractPaginatedStorage storage;

  /**
   * Flags which indicates whether {@link #wowCache} field is initialized on demand.
   * We use separate flag because null can be valid value.
   */
  private volatile boolean   wowCacheInitialized;
  /**
   * Instance of write cache which belongs to current storage, may be null if it is not
   * disk based storage.
   * Initialized on demand.
   */
  private volatile OWOWCache wowCache;

  /**
   * Flags which indicates whether {@link #readCache} field is initialized on demand.
   * We use separate flag because null can be valid value.
   */
  private volatile boolean  readCacheInitialized;
  /**
   * Instance of read cache which belongs to current storage, may be null if it is not
   * disk based storage.
   * Initialized on demand.
   */
  private volatile O2QCache readCache;

  private volatile boolean writeAheadLogInitialized;

  /**
   * @param intervalBetweenSnapshots Interval between snapshots of performance data for each thread statistic.
   * @param cleanUpInterval          Interval between time series of performance data.
   *
   * @see OSessionStoragePerformanceStatistic
   */
  public OPerformanceStatisticManager(OAbstractPaginatedStorage storage, long intervalBetweenSnapshots, long cleanUpInterval) {
    this.intervalBetweenSnapshots = intervalBetweenSnapshots;
    this.cleanUpInterval = cleanUpInterval;
    this.storage = storage;
  }

  /**
   * @return Returns current instance of write cache and initializes local reference if such one is not initialized yet.
   */
  private OWOWCache getWowCache() {
    if (wowCacheInitialized)
      return wowCache;

    final OWriteCache writeCache = storage.getWriteCache();
    if (writeCache instanceof OWOWCache)
      this.wowCache = (OWOWCache) writeCache;
    else
      this.wowCache = null;

    wowCacheInitialized = true;

    return this.wowCache;
  }

  /**
   * @return Returns current instance of read cache and initializes local reference if such one is not initialized yet.
   */
  private O2QCache gerReadCache() {
    if (readCacheInitialized)
      return readCache;

    final OReadCache cache = storage.getReadCache();
    if (cache instanceof O2QCache) {
      this.readCache = (O2QCache) cache;
    } else {
      this.readCache = null;
    }

    readCacheInitialized = true;

    return readCache;
  }

  /**
   * Starts performance monitoring only for single thread.
   * After call of this method you can not start system wide monitoring till call of {@link #stopThreadMonitoring()} is performed.
   */
  public void startThreadMonitoring() {
    switchLock.acquireWriteLock();
    try {
      if (enabled)
        throw new IllegalStateException("Monitoring is already started on system level and can not be started on thread level");

      enabledForCurrentThread.set(true);
      statistics.put(Thread.currentThread(), new OSessionStoragePerformanceStatistic(intervalBetweenSnapshots, Long.MAX_VALUE));
    } finally {
      switchLock.releaseWriteLock();
    }
  }

  /**
   * @return Container for gathered performance statistic or null if {@link #startThreadMonitoring()} is not called.
   */
  public OSessionStoragePerformanceStatistic stopThreadMonitoring() {
    switchLock.acquireWriteLock();
    try {
      enabledForCurrentThread.set(false);

      final Thread currentThread = Thread.currentThread();
      return statistics.remove(currentThread);
    } finally {
      switchLock.releaseWriteLock();
    }
  }

  /**
   * Starts performance monitoring only for whole system.
   * After call of this method you can not start monitoring on thread level till call of {@link #stopMonitoring()} is performed.
   */
  public void startMonitoring() {
    switchLock.acquireWriteLock();
    try {
      if (!statistics.isEmpty() && !enabled)
        throw new IllegalStateException("Monitoring is already started on thread level and can not be started on system level");

      deadThreadsStatistic = null;
      postMeasurementStatistic = null;

      enabled = true;
    } finally {
      switchLock.releaseWriteLock();
    }
  }

  /**
   * Stops monitoring of performance statistic for whole system.
   */
  public void stopMonitoring() {
    switchLock.acquireWriteLock();
    try {
      enabled = false;

      final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
      final Map<String, PerformanceCountersHolder> componentCountersHolder = new HashMap<>();

      WritCacheCountersHolder writCacheCountersHolder = deadThreadsStatistic.writCacheCountersHolder;
      StorageCountersHolder storageCountersHolder = deadThreadsStatistic.storageCountersHolder;
      WALCountersHolder walCountersHolder = deadThreadsStatistic.walCountersHolder;

      deadThreadsStatistic.countersHolder.pushData(countersHolder);
      componentCountersHolder.putAll(deadThreadsStatistic.countersByComponents);

      deadThreadsStatistic = null;

      for (OSessionStoragePerformanceStatistic statistic : statistics.values()) {
        statistic.pushSystemCounters(countersHolder);
        statistic.pushComponentCounters(componentCountersHolder);
        writCacheCountersHolder = statistic.pushWriteCacheCounters(writCacheCountersHolder);
        storageCountersHolder = statistic.pushStorageCounters(storageCountersHolder);
        walCountersHolder = statistic.pushWALCounters(walCountersHolder);
      }

      statistics.clear();

      postMeasurementStatistic = new ImmutableStatistic(countersHolder, componentCountersHolder, writCacheCountersHolder,
          storageCountersHolder, walCountersHolder);
    } finally {
      switchLock.releaseWriteLock();
    }
  }

  /**
   * Registers JMX bean for current manager.
   *
   * @param storageName Name of storage of given manager
   * @param storageId   Id of storage of given manager
   *
   * @see OStorage#getName()
   * @see OIdentifiableStorage#getId()
   */
  public void registerMBean(String storageName, int storageId) {
    if (mbeanIsRegistered.compareAndSet(false, true)) {
      try {
        final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        final ObjectName mbeanName = new ObjectName(getMBeanName(storageName, storageId));
        if (!server.isRegistered(mbeanName)) {
          server.registerMBean(new OPerformanceStatisticManagerMBean(this), mbeanName);
        } else {
          mbeanIsRegistered.set(false);
          OLogManager.instance().warn(this,
              "MBean with name %s has already registered. Probably your system was not shutdown correctly"
                  + " or you have several running applications which use OrientDB engine inside", mbeanName.getCanonicalName());
        }

      } catch (MalformedObjectNameException | InstanceAlreadyExistsException | NotCompliantMBeanException | MBeanRegistrationException e) {
        throw OException.wrapException(new OStorageException("Error during registration of profiler MBean"), e);
      }
    }
  }

  private String getMBeanName(String storageName, int storageId) {
    return MBEAN_PREFIX + ",name=" + ObjectName.quote(storageName) + ",id=" + storageId;
  }

  /**
   * Deregisters JMX bean for current manager.
   *
   * @param storageName Name of storage of given manager
   * @param storageId   Id of storage of given manager
   *
   * @see OStorage#getName()
   * @see OIdentifiableStorage#getId()
   */
  public void unregisterMBean(String storageName, int storageId) {
    if (storageName == null) {
      OLogManager.instance().warnNoDb(this, "Can not unregister MBean for performance statistics, storage name is null");
    }
    if (mbeanIsRegistered.compareAndSet(true, false)) {
      try {
        final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        final ObjectName mbeanName = new ObjectName(getMBeanName(storageName, storageId));
        server.unregisterMBean(mbeanName);
      } catch (MalformedObjectNameException | InstanceNotFoundException | MBeanRegistrationException e) {
        throw OException.wrapException(new OStorageException("Error during unregistration of profiler MBean"), e);
      }
    }
  }

  /**
   * Registers component as such one which participates in performance monitoring.
   *
   * @param component Component which participates in performance monitoring.
   */
  public void registerComponent(String component) {
    componentNames.add(component);
  }

  /**
   * @return Set of names of components for which performance statistic is gathered.
   */
  public Set<String> getComponentNames() {
    return new HashSet<>(componentNames);
  }

  /**
   * @return Instance of performance container which is used to gathering data about storage performance statistic or
   * null if none of both methods {@link #startMonitoring()} or {@link #startThreadMonitoring()} are called.
   */
  public OSessionStoragePerformanceStatistic getSessionPerformanceStatistic() {
    if (!enabled && !enabledForCurrentThread.get())
      return null;

    switchLock.acquireReadLock();
    try {
      if (!enabled && !enabledForCurrentThread.get())
        return null;

      final Thread currentThread = Thread.currentThread();
      OSessionStoragePerformanceStatistic performanceStatistic = statistics.get(currentThread);

      if (performanceStatistic != null) {
        return performanceStatistic;
      }

      performanceStatistic = new OSessionStoragePerformanceStatistic(intervalBetweenSnapshots, enabled ? cleanUpInterval : -1);

      statistics.put(currentThread, performanceStatistic);

      return performanceStatistic;
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * Average amount of pages which were read from cache for component with given name during single data operation.
   * If null value is passed or data for component with passed in name does not exist then -1 will be returned.
   *
   * @param componentName Name of component data of which should be returned. Name is case sensitive.
   *
   * @return Average amount of pages which were read from cache for component with given name during single data operation or value
   * which is less than 0, which means that value cannot be calculated.
   */
  public long getAmountOfPagesPerOperation(String componentName) {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder componentCountersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchComponentCounters(componentName, componentCountersHolder);
        return componentCountersHolder.getAmountOfPagesPerOperation();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;

        if (post == null)
          return -1;

        final PerformanceCountersHolder holder = post.countersByComponents.get(componentName);
        if (holder == null)
          return -1;

        return holder.getAmountOfPagesPerOperation();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Percent of cache hits or value which is less than 0, which means that value cannot be calculated.
   */
  public int getCacheHits() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchSystemCounters(countersHolder);
        return countersHolder.getCacheHits();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;

        if (post == null)
          return -1;

        return post.countersHolder.getCacheHits();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * Percent of cache hits for component name of which is passed as method argument. If null value is passed then value for whole
   * system will be returned. If data for component with passed in name does not exist then -1 will be returned.
   *
   * @param componentName Name of component data of which should be returned. Name is case sensitive.
   *
   * @return Percent of cache hits or value which is less than 0, which means that value cannot be calculated.
   */
  public int getCacheHits(String componentName) {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchComponentCounters(componentName, countersHolder);
        return countersHolder.getCacheHits();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final PerformanceCountersHolder holder = post.countersByComponents.get(componentName);
        if (holder != null)
          return holder.getCacheHits();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Average time of commit of atomic operation in nanoseconds or value which is less than 0, which means that value cannot
   * be calculated.
   */
  public long getCommitTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchSystemCounters(countersHolder);
        return countersHolder.getCommitTime();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        return post.countersHolder.getCommitTime();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Read speed of data in pages per second on cache level or value which is less than 0, which means that value cannot be
   * calculated.
   */
  public long getReadSpeedFromCacheInPages() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchSystemCounters(countersHolder);
        return countersHolder.getReadSpeedFromCacheInPages();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        return post.countersHolder.getReadSpeedFromCacheInPages();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * Read speed of data in pages per second on cache level for component name of which is passed as method argument. If null value
   * is passed then value for whole system will be returned. If data for component with passed in name does not exist then
   * -1 will be returned.
   *
   * @param componentName Name of component data of which should be returned. Name is case sensitive.
   *
   * @return Read speed of data in pages per second on cache level or value which is less than 0, which means that value cannot be
   * calculated.
   */
  public long getReadSpeedFromCacheInPages(String componentName) {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchComponentCounters(componentName, countersHolder);
        return countersHolder.getReadSpeedFromCacheInPages();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final PerformanceCountersHolder holder = post.countersByComponents.get(componentName);
        if (holder == null)
          return -1;

        return holder.getReadSpeedFromCacheInPages();
      }

    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Read speed of data from file system in pages per second or value which is less than 0, which means that value cannot be
   * calculated.
   */
  public long getReadSpeedFromFileInPages() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchSystemCounters(countersHolder);
        return countersHolder.getReadSpeedFromFileInPages();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        return post.countersHolder.getReadSpeedFromFileInPages();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * Read speed of data from file system in pages for component name of which is passed as method argument. If null value is passed
   * then value for whole system will be returned. If data for component with passed in name does not exist then -1
   * will be returned.
   *
   * @param componentName Name of component data of which should be returned. Name is case sensitive.
   *
   * @return Read speed of data from file system in pages per second or value which is less than 0, which means that value cannot be
   * calculated.
   */
  public long getReadSpeedFromFileInPages(String componentName) {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchComponentCounters(componentName, countersHolder);
        return countersHolder.getReadSpeedFromFileInPages();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final PerformanceCountersHolder holder = post.countersByComponents.get(componentName);
        if (holder == null)
          return -1;

        return holder.getReadSpeedFromFileInPages();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Write speed of data in pages per second on cache level or value which is less than 0, which means that value cannot be
   * calculated.
   */
  public long getWriteSpeedInCacheInPages() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchSystemCounters(countersHolder);
        return countersHolder.getWriteSpeedInCacheInPages();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;

        if (post == null)
          return -1;

        return post.countersHolder.getWriteSpeedInCacheInPages();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * Write speed of data in pages per second on cache level for component name of which is passed as method argument. If null value
   * is passed then value for whole system will be returned. If data for component with passed in name does not exist then
   * -1 will be returned.
   *
   * @param componentName Name of component data of which should be returned. Name is case sensitive.
   *
   * @return Write speed of data in pages per second on cache level or value which is less than 0, which means that value cannot be
   * calculated.
   */
  public long getWriteSpeedInCacheInPages(String componentName) {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
        fetchComponentCounters(componentName, countersHolder);
        return countersHolder.getWriteSpeedInCacheInPages();
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final PerformanceCountersHolder holder = post.countersByComponents.get(componentName);
        if (holder == null)
          return -1;

        return holder.getWriteSpeedInCacheInPages();
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Amount of pages which are flushed during "page flush" operation of write cache or -1 if value is
   * undefined.
   */
  public long getWriteCachePagesPerFlush() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        WritCacheCountersHolder holder = fetchWriteCacheCounters();
        if (holder != null)
          return holder.getPagesPerFlush();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WritCacheCountersHolder holder = post.writCacheCountersHolder;
        if (holder != null)
          return holder.getPagesPerFlush();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Amount of time which is spent on each "page flush" operation of write cache or -1 if
   * value is undefined.
   */
  public long getWriteCacheFlushOperationsTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        WritCacheCountersHolder holder = fetchWriteCacheCounters();
        if (holder != null)
          return holder.getFlushOperationsTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WritCacheCountersHolder holder = post.writCacheCountersHolder;
        if (holder != null)
          return holder.getFlushOperationsTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Amount of time which is spent on "fuzzy checkpoint" operation or -1 if
   * value if undefined.
   */
  public long getWriteCacheFuzzyCheckpointTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        WritCacheCountersHolder holder = fetchWriteCacheCounters();
        if (holder != null)
          return holder.getFuzzyCheckpointTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WritCacheCountersHolder holder = post.writCacheCountersHolder;
        if (holder != null)
          return holder.getFuzzyCheckpointTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Time which is spent on full checkpoint operation, or -1 if value is undefined
   */
  public long getFullCheckpointTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        StorageCountersHolder holder = fetchStorageCounters();
        if (holder != null)
          return holder.getFullCheckpointTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final StorageCountersHolder holder = post.storageCountersHolder;
        if (holder != null)
          return holder.getFullCheckpointTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Amount of times full checkpoint operations is called on storage, or -1 if this value is undefined.
   */
  public long getFullCheckpointCount() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        fullCheckpointCount = storage.getFullCheckpointCount();
        return fullCheckpointCount;
      } else {
        return fullCheckpointCount;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Size of read cache in bytes or -1 if value is undefined.
   */
  public long getReadCacheSize() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final O2QCache cache = gerReadCache();
        if (cache != null)
          readCacheSize = cache.getUsedMemory();

        return readCacheSize;
      } else {
        return readCacheSize;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Size of write cache in bytes or -1 if value is undefined.
   */
  public long getWriteCacheSize() {
    switchLock.acquireReadLock();
    try {

      if (enabled) {
        final OWOWCache cache = getWowCache();
        if (cache != null) {
          writeCacheSize = cache.getWriteCacheSize();
        }

        return writeCacheSize;
      } else {
        return writeCacheSize;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Size of part of write cache which exclusively belongs to only this cache or -1 if value is undefined
   */
  public long getExclusiveWriteCacheSize() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final OWOWCache cache = getWowCache();
        if (cache != null) {
          exclusiveWriteCacheSize = cache.getExclusiveWriteCacheSize();
        }

        return exclusiveWriteCacheSize;
      } else {
        return exclusiveWriteCacheSize;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Amount of times when size of write cache is not enough to keep dirty pages and flush of write cache is forced, or
   * -1 if this value is undefined.
   */
  public long getWriteCacheOverflowCount() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final OWOWCache cache = getWowCache();
        if (cache != null) {
          writeCacheOverflowCount = cache.getCacheOverflowCount();
        }

        return writeCacheOverflowCount;
      } else {
        return writeCacheOverflowCount;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return time which is spent on logging of single record or -1 if value is undefined.
   */
  public long getWALLogRecordTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final WALCountersHolder holder = fetchWALCounters();
        if (holder != null)
          return holder.getLogTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WALCountersHolder holder = post.walCountersHolder;
        if (holder != null)
          return holder.getLogTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return Time which is spent on logging of "start atomic operation" record or -1 if value is
   * undefined.
   */
  public long getWALStartAOLogRecordTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final WALCountersHolder holder = fetchWALCounters();
        if (holder != null)
          return holder.getStartAOTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WALCountersHolder holder = post.walCountersHolder;
        if (holder != null)
          return holder.getStartAOTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return time which is spent on logging of  "stop atomic operation" record or -1
   * if value is undefined.
   */
  public long getWALStopAOLogRecordTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final WALCountersHolder holder = fetchWALCounters();
        if (holder != null)
          return holder.getStopAOTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WALCountersHolder holder = post.walCountersHolder;
        if (holder != null)
          return holder.getStopAOTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * @return time which is spent on flush of WAL cache or -1 if value is undefined
   */
  public long getWALFlushTime() {
    switchLock.acquireReadLock();
    try {
      if (enabled) {
        final WALCountersHolder holder = fetchWALCounters();

        if (holder != null)
          return holder.getFlushTime();

        return -1;
      } else {
        final ImmutableStatistic post = postMeasurementStatistic;
        if (post == null)
          return -1;

        final WALCountersHolder holder = post.walCountersHolder;
        if (holder != null)
          return holder.getFlushTime();

        return -1;
      }
    } finally {
      switchLock.releaseReadLock();
    }
  }

  /**
   * Iterates over all live threads and accumulates write performance statics gathered form threads,
   * also accumulates statistic from dead threads which were alive when when gathering of performance measurements is started.
   *
   * @return Aggregated write cache performance statistic
   */
  private WritCacheCountersHolder fetchWriteCacheCounters() {
    //go through all threads and accumulate statistic only for live threads
    //all dead threads will be removed and statistics from them will be
    //later accumulated in #deadThreadsStatistic field, then result statistic from this field
    //will be aggregated to countersHolder

    //To decrease inter thread communication delay we fetch snapshots first
    //and only after that we aggregate data from immutable snapshots
    final Collection<ORawPair<Thread, PerformanceSnapshot>> snapshots = new ArrayList<>(statistics.size());

    final Collection<Thread> threadsToRemove = new ArrayList<>();
    for (Map.Entry<Thread, OSessionStoragePerformanceStatistic> entry : statistics.entrySet()) {
      final Thread thread = entry.getKey();

      final OSessionStoragePerformanceStatistic statistic = entry.getValue();
      snapshots.add(new ORawPair<>(thread, statistic.getSnapshot()));
    }

    WritCacheCountersHolder holder = null;
    for (ORawPair<Thread, PerformanceSnapshot> pair : snapshots) {
      final Thread thread = pair.getFirst();

      if (thread.isAlive()) {
        final PerformanceSnapshot snapshot = pair.getSecond();

        if (snapshot.writCacheCountersHolder != null) {
          if (holder == null)
            holder = new WritCacheCountersHolder();

          snapshot.writCacheCountersHolder.pushData(holder);
        }
      } else {
        threadsToRemove.add(thread);
      }
    }

    if (!threadsToRemove.isEmpty()) {
      updateDeadThreadsStatistic(threadsToRemove);
    }

    final ImmutableStatistic ds = deadThreadsStatistic;
    if (ds != null) {
      final WritCacheCountersHolder wch = ds.writCacheCountersHolder;
      if (wch != null) {
        if (holder == null)
          holder = new WritCacheCountersHolder();

        wch.pushData(holder);
      }
    }

    return holder;
  }

  /**
   * Iterates over all live threads and accumulates storage performance statics gathered form threads,
   * also accumulates statistic from dead threads which were alive when when gathering of performance measurements is started.
   *
   * @return Aggregated storage performance statistic
   */
  private StorageCountersHolder fetchStorageCounters() {
    //go through all threads and accumulate statistic only for live threads
    //all dead threads will be removed and statistics from them will be
    //later accumulated in #deadThreadsStatistic field, then result statistic from this field
    //will be aggregated to countersHolder

    //To decrease inter thread communication delay we fetch snapshots first
    //and only after that we aggregate data from immutable snapshots
    final Collection<ORawPair<Thread, PerformanceSnapshot>> snapshots = new ArrayList<>(statistics.size());

    final Collection<Thread> threadsToRemove = new ArrayList<>();
    for (Map.Entry<Thread, OSessionStoragePerformanceStatistic> entry : statistics.entrySet()) {
      final Thread thread = entry.getKey();

      final OSessionStoragePerformanceStatistic statistic = entry.getValue();
      snapshots.add(new ORawPair<>(thread, statistic.getSnapshot()));
    }

    StorageCountersHolder holder = null;
    for (ORawPair<Thread, PerformanceSnapshot> pair : snapshots) {
      final Thread thread = pair.getFirst();

      if (thread.isAlive()) {
        final PerformanceSnapshot snapshot = pair.getSecond();

        if (snapshot.storageCountersHolder != null) {
          if (holder == null)
            holder = new StorageCountersHolder();

          snapshot.storageCountersHolder.pushData(holder);
        }
      } else {
        threadsToRemove.add(thread);
      }
    }

    if (!threadsToRemove.isEmpty()) {
      updateDeadThreadsStatistic(threadsToRemove);
    }

    final ImmutableStatistic ds = deadThreadsStatistic;
    if (ds != null) {
      final StorageCountersHolder sch = ds.storageCountersHolder;
      if (sch != null) {
        if (holder == null)
          holder = new StorageCountersHolder();

        sch.pushData(holder);
      }
    }

    return holder;
  }

  /**
   * Iterates over all live threads and accumulates write ahead log performance statics gathered form threads,
   * also accumulates statistic from dead threads which were alive when when gathering of performance measurements is started.
   *
   * @return Aggregated write ahead log performance statistic
   */
  private WALCountersHolder fetchWALCounters() {
    //go through all threads and accumulate statistic only for live threads
    //all dead threads will be removed and statistics from them will be
    //later accumulated in #deadThreadsStatistic field, then result statistic from this field
    //will be aggregated to countersHolder

    //To decrease inter thread communication delay we fetch snapshots first
    //and only after that we aggregate data from immutable snapshots
    final Collection<ORawPair<Thread, PerformanceSnapshot>> snapshots = new ArrayList<>(statistics.size());

    final Collection<Thread> threadsToRemove = new ArrayList<>();
    for (Map.Entry<Thread, OSessionStoragePerformanceStatistic> entry : statistics.entrySet()) {
      final Thread thread = entry.getKey();

      final OSessionStoragePerformanceStatistic statistic = entry.getValue();
      snapshots.add(new ORawPair<>(thread, statistic.getSnapshot()));
    }

    WALCountersHolder holder = null;
    for (ORawPair<Thread, PerformanceSnapshot> pair : snapshots) {
      final Thread thread = pair.getFirst();

      if (thread.isAlive()) {
        final PerformanceSnapshot snapshot = pair.getSecond();

        if (snapshot.walCountersHolder != null) {
          if (holder == null)
            holder = new WALCountersHolder();

          snapshot.walCountersHolder.pushData(holder);
        }
      } else {
        threadsToRemove.add(thread);
      }
    }

    if (!threadsToRemove.isEmpty()) {
      updateDeadThreadsStatistic(threadsToRemove);
    }

    final ImmutableStatistic ds = deadThreadsStatistic;
    if (ds != null) {
      final WALCountersHolder wch = ds.walCountersHolder;
      if (wch != null) {
        if (holder == null)
          holder = new WALCountersHolder();

        wch.pushData(holder);
      }
    }

    return holder;
  }

  /**
   * Iterates over all live threads and accumulates performance statics gathered form threads on system level,
   * also accumulates statistic from dead threads which were alive when when gathering of performance measurements is started.
   *
   * @param countersHolder Holder which is used to accumulate all performance statistic data
   */
  private void fetchSystemCounters(PerformanceCountersHolder countersHolder) {
    //go through all threads and accumulate statistic only for live threads
    //all dead threads will be removed and statistics from them will be
    //later accumulated in #deadThreadsStatistic field, then result statistic from this field
    //will be aggregated to countersHolder

    //To decrease inter thread communication delay we fetch snapshots first
    //and only after that we aggregate data from immutable snapshots
    final Collection<ORawPair<Thread, PerformanceSnapshot>> snapshots = new ArrayList<>(statistics.size());

    final Collection<Thread> threadsToRemove = new ArrayList<>();
    for (Map.Entry<Thread, OSessionStoragePerformanceStatistic> entry : statistics.entrySet()) {
      final Thread thread = entry.getKey();
      final OSessionStoragePerformanceStatistic statistic = entry.getValue();
      snapshots.add(new ORawPair<>(thread, statistic.getSnapshot()));
    }

    for (ORawPair<Thread, PerformanceSnapshot> pair : snapshots) {
      final Thread thread = pair.getFirst();

      if (thread.isAlive()) {
        final PerformanceSnapshot snapshot = pair.getSecond();
        snapshot.performanceCountersHolder.pushData(countersHolder);
      } else {
        threadsToRemove.add(thread);
      }
    }

    if (!threadsToRemove.isEmpty()) {
      updateDeadThreadsStatistic(threadsToRemove);
    }

    final ImmutableStatistic ds = deadThreadsStatistic;
    if (ds != null) {
      final PerformanceCountersHolder dch = ds.countersHolder;
      dch.pushData(countersHolder);
    }
  }

  /**
   * Iterates over all live threads and accumulates performance statics gathered form threads for provided component,
   * also accumulates statistic from dead threads which were alive when when gathering of performance measurements is started.
   *
   * @param componentCountersHolder Holder which is used to accumulate all performance statistic data for given component
   * @param componentName           Name of component
   */
  private void fetchComponentCounters(String componentName, PerformanceCountersHolder componentCountersHolder) {
    //go through all threads and accumulate statistic only for live threads
    //all dead threads will be removed and statistics from them will be
    //later accumulated in #deadThreadsStatistic field, then result statistic from this field
    //will be aggregated to componentCountersHolder

    //To decrease inter thread communication delay we fetch snapshots first
    //and only after that we aggregate data from immutable snapshots
    final Collection<ORawPair<Thread, PerformanceSnapshot>> snapshots = new ArrayList<>(statistics.size());

    final List<Thread> threadsToRemove = new ArrayList<>();
    for (Map.Entry<Thread, OSessionStoragePerformanceStatistic> entry : statistics.entrySet()) {
      final Thread thread = entry.getKey();
      final OSessionStoragePerformanceStatistic statistic = entry.getValue();
      snapshots.add(new ORawPair<>(thread, statistic.getSnapshot()));
    }

    for (ORawPair<Thread, PerformanceSnapshot> pair : snapshots) {
      final Thread thread = pair.getFirst();
      if (thread.isAlive()) {
        final PerformanceSnapshot snapshot = pair.getSecond();
        final PerformanceCountersHolder holder = snapshot.countersByComponent.get(componentName);
        if (holder != null)
          holder.pushData(componentCountersHolder);
      } else {
        threadsToRemove.add(thread);
      }
    }

    if (!threadsToRemove.isEmpty()) {
      updateDeadThreadsStatistic(threadsToRemove);
    }

    final ImmutableStatistic ds = deadThreadsStatistic;
    if (ds != null) {
      final PerformanceCountersHolder dch = ds.countersByComponents.get(componentName);
      if (dch != null) {
        dch.pushData(componentCountersHolder);
      }
    }
  }

  /**
   * Removes provided dead threads from {@link #statistics} field and accumulates data from them in {@link #deadThreadsStatistic}.
   *
   * @param threadsToRemove Dead threads statistic of which should be moved to {@link #deadThreadsStatistic} field.
   */
  private void updateDeadThreadsStatistic(Collection<Thread> threadsToRemove) {
    deadThreadsUpdateLock.lock();
    try {
      //we accumulate all statistic in intermediate fields and only then put
      //results in #deadThreadsStatistic field to preserve thread safety features
      final ImmutableStatistic oldDS = deadThreadsStatistic;
      final PerformanceCountersHolder countersHolder = ComponentType.GENERAL.newCountersHolder();
      final Map<String, PerformanceCountersHolder> countersByComponents = new HashMap<>();

      WritCacheCountersHolder writeCacheCountersHolder = null;
      StorageCountersHolder storageCountersHolder = null;
      WALCountersHolder walCountersHolder = null;

      //fetch data from old statistic first
      if (oldDS != null) {
        oldDS.countersHolder.pushData(countersHolder);

        for (Map.Entry<String, PerformanceCountersHolder> oldEntry : oldDS.countersByComponents.entrySet()) {
          final PerformanceCountersHolder holder = oldEntry.getValue().newInstance();
          oldEntry.getValue().pushData(holder);

          countersByComponents.put(oldEntry.getKey(), holder);
        }

        if (oldDS.writCacheCountersHolder != null) {
          writeCacheCountersHolder = new WritCacheCountersHolder();
          oldDS.writCacheCountersHolder.pushData(writeCacheCountersHolder);
        }

        if (oldDS.storageCountersHolder != null) {
          storageCountersHolder = new StorageCountersHolder();
          oldDS.storageCountersHolder.pushData(storageCountersHolder);
        }

        if (oldDS.walCountersHolder != null) {
          walCountersHolder = new WALCountersHolder();
          oldDS.walCountersHolder.pushData(walCountersHolder);
        }
      }

      //remove all threads from active statistic and put all in #deadThreadsStatistic field
      for (Thread deadThread : threadsToRemove) {
        final OSessionStoragePerformanceStatistic sessionStoragePerformanceStatistic = statistics.remove(deadThread);
        if (sessionStoragePerformanceStatistic != null) {
          sessionStoragePerformanceStatistic.pushSystemCounters(countersHolder);
          sessionStoragePerformanceStatistic.pushComponentCounters(countersByComponents);
          writeCacheCountersHolder = sessionStoragePerformanceStatistic.pushWriteCacheCounters(writeCacheCountersHolder);
          storageCountersHolder = sessionStoragePerformanceStatistic.pushStorageCounters(storageCountersHolder);
          walCountersHolder = sessionStoragePerformanceStatistic.pushWALCounters(walCountersHolder);
        }
      }

      deadThreadsStatistic = new ImmutableStatistic(countersHolder, countersByComponents, writeCacheCountersHolder,
          storageCountersHolder, walCountersHolder);
    } finally {
      deadThreadsUpdateLock.unlock();
    }
  }

  private final class ImmutableStatistic {
    final PerformanceCountersHolder              countersHolder;
    final Map<String, PerformanceCountersHolder> countersByComponents;
    final WritCacheCountersHolder                writCacheCountersHolder;
    final StorageCountersHolder                  storageCountersHolder;
    final WALCountersHolder                      walCountersHolder;

    public ImmutableStatistic(PerformanceCountersHolder countersHolder, Map<String, PerformanceCountersHolder> countersByComponents,
        WritCacheCountersHolder writCacheCountersHolder, StorageCountersHolder storageCountersHolder,
        WALCountersHolder walCountersHolder) {
      this.countersHolder = countersHolder;
      this.countersByComponents = countersByComponents;
      this.writCacheCountersHolder = writCacheCountersHolder;
      this.storageCountersHolder = storageCountersHolder;
      this.walCountersHolder = walCountersHolder;
    }

  }
}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://orientdb.com
 *
 */

package com.orientechnologies.orient.core.storage.index.sbtree.singlevalue;

import com.orientechnologies.common.comparator.ODefaultComparator;
import com.orientechnologies.common.serialization.types.OBinarySerializer;
import com.orientechnologies.common.serialization.types.OByteSerializer;
import com.orientechnologies.common.serialization.types.OIntegerSerializer;
import com.orientechnologies.common.serialization.types.OLongSerializer;
import com.orientechnologies.common.serialization.types.OShortSerializer;
import com.orientechnologies.orient.core.encryption.OEncryption;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.storage.cache.OCacheEntry;
import com.orientechnologies.orient.core.storage.impl.local.paginated.base.ODurablePage;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * @author Andrey Lomakin (a.lomakin-at-orientdb.com)
 * @since 8/7/13
 */
final class OSBTreeBucketSingleValue<K> extends ODurablePage {
  private static final int RID_SIZE = OShortSerializer.SHORT_SIZE + OLongSerializer.LONG_SIZE;

  private static final int FREE_POINTER_OFFSET  = NEXT_FREE_POSITION;
  private static final int SIZE_OFFSET          = FREE_POINTER_OFFSET + OIntegerSerializer.INT_SIZE;
  private static final int IS_LEAF_OFFSET       = SIZE_OFFSET + OIntegerSerializer.INT_SIZE;
  private static final int LEFT_SIBLING_OFFSET  = IS_LEAF_OFFSET + OByteSerializer.BYTE_SIZE;
  private static final int RIGHT_SIBLING_OFFSET = LEFT_SIBLING_OFFSET + OLongSerializer.LONG_SIZE;

  private static final int POSITIONS_ARRAY_OFFSET = RIGHT_SIBLING_OFFSET + OLongSerializer.LONG_SIZE;

  private final boolean isLeaf;

  private final OBinarySerializer<K> keySerializer;

  private final OType[] keyTypes;

  private final Comparator<? super K> comparator = ODefaultComparator.INSTANCE;

  private final OEncryption encryption;

  OSBTreeBucketSingleValue(final OCacheEntry cacheEntry, final boolean isLeaf, final OBinarySerializer<K> keySerializer,
      final OType[] keyTypes, final OEncryption encryption) {
    super(cacheEntry);

    this.isLeaf = isLeaf;
    this.keySerializer = keySerializer;
    this.keyTypes = keyTypes;
    this.encryption = encryption;

    setIntValue(FREE_POINTER_OFFSET, MAX_PAGE_SIZE_BYTES);
    setIntValue(SIZE_OFFSET, 0);

    setByteValue(IS_LEAF_OFFSET, (byte) (isLeaf ? 1 : 0));
    setLongValue(LEFT_SIBLING_OFFSET, -1);
    setLongValue(RIGHT_SIBLING_OFFSET, -1);
  }

  OSBTreeBucketSingleValue(final OCacheEntry cacheEntry, final OBinarySerializer<K> keySerializer, final OType[] keyTypes,
      final OEncryption encryption) {
    super(cacheEntry);
    this.keyTypes = keyTypes;
    this.encryption = encryption;

    this.isLeaf = getByteValue(IS_LEAF_OFFSET) > 0;
    this.keySerializer = keySerializer;
  }

  public boolean isEmpty() {
    return size() == 0;
  }

  public int find(final K key) {
    int low = 0;
    int high = size() - 1;

    while (low <= high) {
      final int mid = (low + high) >>> 1;
      final K midVal = getKey(mid);
      final int cmp = comparator.compare(midVal, key);

      if (cmp < 0) {
        low = mid + 1;
      } else if (cmp > 0) {
        high = mid - 1;
      } else {
        return mid; // key found
      }
    }

    return -(low + 1); // key not found.
  }

  public void remove(final int entryIndex, final byte[] oldRawKey) {
    final int entryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + entryIndex * OIntegerSerializer.INT_SIZE);
    final int keySize;

    if (oldRawKey == null) {
      if (encryption == null) {
        keySize = getObjectSizeInDirectMemory(keySerializer, entryPosition);
      } else {
        final int encryptionSize = getIntValue(entryPosition);
        keySize = OIntegerSerializer.INT_SIZE + encryptionSize;
      }
    } else {
      keySize = oldRawKey.length;
    }

    final int entrySize;
    if (isLeaf) {
      entrySize = keySize + RID_SIZE;
    } else {
      throw new IllegalStateException("Remove is applies to leaf buckets only");
    }

    int size = getIntValue(SIZE_OFFSET);
    if (entryIndex < size - 1) {
      moveData(POSITIONS_ARRAY_OFFSET + (entryIndex + 1) * OIntegerSerializer.INT_SIZE,
          POSITIONS_ARRAY_OFFSET + entryIndex * OIntegerSerializer.INT_SIZE, (size - entryIndex - 1) * OIntegerSerializer.INT_SIZE);
    }

    size--;
    setIntValue(SIZE_OFFSET, size);

    final int freePointer = getIntValue(FREE_POINTER_OFFSET);
    if (size > 0 && entryPosition > freePointer) {
      moveData(freePointer, freePointer + entrySize, entryPosition - freePointer);
    }

    setIntValue(FREE_POINTER_OFFSET, freePointer + entrySize);

    int currentPositionOffset = POSITIONS_ARRAY_OFFSET;

    for (int i = 0; i < size; i++) {
      final int currentEntryPosition = getIntValue(currentPositionOffset);
      if (currentEntryPosition < entryPosition) {
        setIntValue(currentPositionOffset, currentEntryPosition + entrySize);
      }
      currentPositionOffset += OIntegerSerializer.INT_SIZE;
    }
  }

  public int size() {
    return getIntValue(SIZE_OFFSET);
  }

  public SBTreeEntry<K> getEntry(final int entryIndex) {
    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    if (isLeaf) {
      final K key;
      if (encryption == null) {
        key = deserializeFromDirectMemory(keySerializer, entryPosition);

        entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
      } else {
        final int encryptionSize = getIntValue(entryPosition);
        entryPosition += OIntegerSerializer.INT_SIZE;

        final byte[] encryptedKey = getBinaryValue(entryPosition, encryptionSize);
        entryPosition += encryptedKey.length;

        final byte[] serializedKey = encryption.decrypt(encryptedKey);

        key = keySerializer.deserializeNativeObject(serializedKey, 0);
      }

      final int clusterId = getShortValue(entryPosition);
      final long clusterPosition = getLongValue(entryPosition + OShortSerializer.SHORT_SIZE);

      return new SBTreeEntry<>(-1, -1, key, new ORecordId(clusterId, clusterPosition));
    } else {
      final int leftChild = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE;

      final int rightChild = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE;

      final K key;

      if (encryption == null) {
        key = deserializeFromDirectMemory(keySerializer, entryPosition);
      } else {
        final int encryptionSize = getIntValue(entryPosition);
        entryPosition += OIntegerSerializer.INT_SIZE;

        final byte[] encryptedKey = getBinaryValue(entryPosition, encryptionSize);

        final byte[] serializedKey = encryption.decrypt(encryptedKey);

        key = keySerializer.deserializeNativeObject(serializedKey, 0);
      }

      return new SBTreeEntry<>(leftChild, rightChild, key, null);
    }
  }

  int getLeft(final int entryIndex) {
    assert !isLeaf;

    final int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    return getIntValue(entryPosition);
  }

  int getRight(final int entryIndex) {
    assert !isLeaf;

    final int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    return getIntValue(entryPosition + OIntegerSerializer.INT_SIZE);
  }

  byte[] getRawEntry(final int entryIndex) {
    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);
    final int startEntryPosition = entryPosition;

    if (isLeaf) {
      final int keySize;
      if (encryption == null) {
        keySize = getObjectSizeInDirectMemory(keySerializer, entryPosition);
      } else {
        final int encryptedSize = getIntValue(entryPosition);
        keySize = OIntegerSerializer.INT_SIZE + encryptedSize;
      }

      return getBinaryValue(startEntryPosition, keySize + RID_SIZE);
    } else {
      entryPosition += 2 * OIntegerSerializer.INT_SIZE;

      final int keySize;
      if (encryption == null) {
        keySize = getObjectSizeInDirectMemory(keySerializer, entryPosition);
      } else {
        final int encryptedSize = getIntValue(entryPosition);
        keySize = OIntegerSerializer.INT_SIZE + encryptedSize;
      }

      return getBinaryValue(startEntryPosition, keySize + 2 * OIntegerSerializer.INT_SIZE);
    }
  }

  /**
   * Obtains the value stored under the given entry index in this bucket.
   *
   * @param entryIndex the value entry index.
   *
   * @return the obtained value.
   */
  public ORID getValue(final int entryIndex) {
    assert isLeaf;

    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    // skip key
    if (encryption == null) {
      entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedSize = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE + encryptedSize;
    }

    final int clusterId = getShortValue(entryPosition);
    final long clusterPosition = getLongValue(entryPosition + OShortSerializer.SHORT_SIZE);

    return new ORecordId(clusterId, clusterPosition);
  }

  byte[] getRawValue(final int entryIndex) {
    assert isLeaf;

    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    // skip key
    if (encryption == null) {
      entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedSize = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE + encryptedSize;
    }

    return getBinaryValue(entryPosition, RID_SIZE);
  }

  public K getKey(final int index) {
    int entryPosition = getIntValue(index * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    if (!isLeaf) {
      entryPosition += 2 * OIntegerSerializer.INT_SIZE;
    }

    if (encryption == null) {
      return deserializeFromDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedSize = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE;

      final byte[] encryptedKey = getBinaryValue(entryPosition, encryptedSize);
      final byte[] serializedKey = encryption.decrypt(encryptedKey);
      return keySerializer.deserializeNativeObject(serializedKey, 0);
    }
  }

  public boolean isLeaf() {
    return isLeaf;
  }

  public void addAll(final List<byte[]> rawEntries) {
    for (int i = 0; i < rawEntries.size(); i++) {
      appendRawEntry(i, rawEntries.get(i));
    }

    setIntValue(SIZE_OFFSET, rawEntries.size());
  }

  public void shrink(final int newSize) {
    final List<byte[]> rawEntries = new ArrayList<>(newSize);

    for (int i = 0; i < newSize; i++) {
      rawEntries.add(getRawEntry(i));
    }

    setIntValue(FREE_POINTER_OFFSET, MAX_PAGE_SIZE_BYTES);

    int index = 0;
    for (final byte[] entry : rawEntries) {
      appendRawEntry(index, entry);
      index++;
    }

    setIntValue(SIZE_OFFSET, newSize);
  }

  boolean addLeafEntry(final int index, final byte[] serializedKey, final byte[] serializedValue) {
    final int entrySize = serializedKey.length + serializedValue.length;

    assert isLeaf;
    final int size = getIntValue(SIZE_OFFSET);

    int freePointer = getIntValue(FREE_POINTER_OFFSET);
    if (freePointer - entrySize < (size + 1) * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET) {
      return false;
    }

    if (index <= size - 1) {
      moveData(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE,
          POSITIONS_ARRAY_OFFSET + (index + 1) * OIntegerSerializer.INT_SIZE, (size - index) * OIntegerSerializer.INT_SIZE);
    }

    freePointer -= entrySize;

    setIntValue(FREE_POINTER_OFFSET, freePointer);
    setIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE, freePointer);
    setIntValue(SIZE_OFFSET, size + 1);

    setBinaryValue(freePointer, serializedKey);
    setBinaryValue(freePointer + serializedKey.length, serializedValue);

    return true;
  }

  private void appendRawEntry(final int index, final byte[] rawEntry) {
    int freePointer = getIntValue(FREE_POINTER_OFFSET);
    freePointer -= rawEntry.length;

    setIntValue(FREE_POINTER_OFFSET, freePointer);
    setIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE, freePointer);

    setBinaryValue(freePointer, rawEntry);
  }

  public boolean addEntry(final int index, final SBTreeEntry<K> treeEntry, final boolean updateNeighbors) {
    final byte[] serializedKey = keySerializer.serializeNativeAsWhole(treeEntry.key, (Object[]) keyTypes);
    final int keySize;
    byte[] encryptedKey = null;

    if (encryption == null) {
      keySize = keySerializer.getObjectSize(treeEntry.key, (Object[]) keyTypes);
    } else {
      encryptedKey = encryption.encrypt(serializedKey);
      keySize = encryptedKey.length + OIntegerSerializer.INT_SIZE;
    }

    int entrySize = keySize;

    if (isLeaf) {
      entrySize += RID_SIZE;
    } else {
      entrySize += 2 * OIntegerSerializer.INT_SIZE;
    }

    int size = size();
    int freePointer = getIntValue(FREE_POINTER_OFFSET);
    if (freePointer - entrySize < (size + 1) * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET) {
      return false;
    }

    if (index <= size - 1) {
      moveData(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE,
          POSITIONS_ARRAY_OFFSET + (index + 1) * OIntegerSerializer.INT_SIZE, (size - index) * OIntegerSerializer.INT_SIZE);
    }

    freePointer -= entrySize;

    setIntValue(FREE_POINTER_OFFSET, freePointer);
    setIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE, freePointer);
    setIntValue(SIZE_OFFSET, size + 1);

    if (isLeaf) {
      if (encryption == null) {
        freePointer += setBinaryValue(freePointer, serializedKey);
      } else {
        setIntValue(freePointer, encryptedKey.length);
        freePointer += OIntegerSerializer.INT_SIZE;

        freePointer += setBinaryValue(freePointer, encryptedKey);
      }

      freePointer += setShortValue(freePointer, (short) treeEntry.value.getClusterId());
      setLongValue(freePointer, treeEntry.value.getClusterPosition());
    } else {
      freePointer += setIntValue(freePointer, treeEntry.leftChild);
      freePointer += setIntValue(freePointer, treeEntry.rightChild);

      if (encryption == null) {
        setBinaryValue(freePointer, serializedKey);
      } else {
        setIntValue(freePointer, encryptedKey.length);
        freePointer += OIntegerSerializer.INT_SIZE;

        setBinaryValue(freePointer, encryptedKey);
      }

      size++;

      if (updateNeighbors && size > 1) {
        if (index < size - 1) {
          final int nextEntryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + (index + 1) * OIntegerSerializer.INT_SIZE);
          setIntValue(nextEntryPosition, treeEntry.rightChild);
        }

        if (index > 0) {
          final int prevEntryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + (index - 1) * OIntegerSerializer.INT_SIZE);
          setIntValue(prevEntryPosition + OIntegerSerializer.INT_SIZE, treeEntry.leftChild);
        }
      }
    }

    return true;
  }

  void updateValue(final int index, final byte[] value) {
    int entryPosition = getIntValue(index * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET);

    if (encryption == null) {
      entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedValue = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE + encryptedValue;
    }

    setBinaryValue(entryPosition, value);
  }

  void setLeftSibling(final long pageIndex) {
    setLongValue(LEFT_SIBLING_OFFSET, pageIndex);
  }

  public long getLeftSibling() {
    return getLongValue(LEFT_SIBLING_OFFSET);
  }

  void setRightSibling(final long pageIndex) {
    setLongValue(RIGHT_SIBLING_OFFSET, pageIndex);
  }

  public long getRightSibling() {
    return getLongValue(RIGHT_SIBLING_OFFSET);
  }

  public static final class SBTreeEntry<K> implements Comparable<SBTreeEntry<K>> {
    private final Comparator<? super K> comparator = ODefaultComparator.INSTANCE;

    final        int  leftChild;
    final        int  rightChild;
    public final K    key;
    public final ORID value;

    public SBTreeEntry(final int leftChild, final int rightChild, final K key, final ORID value) {
      this.leftChild = leftChild;
      this.rightChild = rightChild;
      this.key = key;
      this.value = value;
    }

    @Override
    public boolean equals(final Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      final SBTreeEntry<?> that = (SBTreeEntry<?>) o;
      return leftChild == that.leftChild && rightChild == that.rightChild && Objects.equals(key, that.key) && Objects
          .equals(value, that.value);
    }

    @Override
    public int hashCode() {
      return Objects.hash(leftChild, rightChild, key, value);
    }

    @Override
    public String toString() {
      return "SBTreeEntry{" + "leftChild=" + leftChild + ", rightChild=" + rightChild + ", key=" + key + ", value=" + value + '}';
    }

    @Override
    public int compareTo(final SBTreeEntry<K> other) {
      return comparator.compare(key, other.key);
    }
  }
}