org.apache.hadoop.hbase.regionserver.metrics.SchemaMetrics.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hbase.regionserver.metrics.SchemaMetrics.java

Source

/*
 * Copyright The Apache Software Foundation
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to you 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.
 */

package org.apache.hadoop.hbase.regionserver.metrics;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.mutable.MutableDouble;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;

/**
 * A collection of metric names in a given column family or a (table, column
 * family) combination. The following "dimensions" are supported:
 * <ul>
 * <li>Table name (optional; enabled based on configuration)</li>
 * <li>Per-column family vs. aggregated. The aggregated mode is only supported
 * when table name is not included.</li>
 * <li>Block category (data, index, bloom filter, etc.)</li>
 * <li>Whether the request is part of a compaction</li>
 * <li>Metric type (read time, block read count, cache hits/misses, etc.)</li>
 * </ul>
 * <p>
 * An instance of this class does not store any metric values. It just allows
 * to determine the correct metric name for each combination of the above
 * dimensions.
 * <p>
 * <table>
 * <tr>
 * <th rowspan="2">Metric key</th>
 * <th colspan="2">Per-table metrics conf setting</th>
 * <th rowspan="2">Description</th>
 * </tr>
 * <tr>
 * <th>On</th>
 * <th>Off</th>
 * </th>
 * <tr>
 *   <td> tbl.T.cf.CF.M </td> <td> Include </td> <td> Skip    </td>
 *   <td> A specific column family of a specific table        </td>
 * </tr>
 * <tr>
 *   <td> tbl.T.M       </td> <td> Skip    </td> <td> Skip    </td>
 *   <td> All column families in the given table              </td>
 * </tr>
 * <tr>
 *   <td> cf.CF.M       </td> <td> Skip    </td> <td> Include </td>
 *   <td> A specific column family in all tables              </td>
 * </tr>
 * <tr>
 *   <td> M             </td> <td> Include </td> <td> Include </td>
 *   <td> All column families in all tables                   </td>
 * </tr>
 * </table>
 */
public class SchemaMetrics {

    public interface SchemaAware {
        public String getTableName();

        public String getColumnFamilyName();

        public SchemaMetrics getSchemaMetrics();
    }

    private static final Log LOG = LogFactory.getLog(SchemaMetrics.class);

    public static enum BlockMetricType {
        // Metric configuration: compactionAware, timeVarying
        READ_TIME("Read", true, true), READ_COUNT("BlockReadCnt", true, false), CACHE_HIT("BlockReadCacheHitCnt",
                true, false), CACHE_MISS("BlockReadCacheMissCnt", true, false),

        CACHE_SIZE("blockCacheSize", false, false), CACHED("blockCacheNumCached", false,
                false), EVICTED("blockCacheNumEvicted", false, false);

        private final String metricStr;
        private final boolean compactionAware;
        private final boolean timeVarying;

        BlockMetricType(String metricStr, boolean compactionAware, boolean timeVarying) {
            this.metricStr = metricStr;
            this.compactionAware = compactionAware;
            this.timeVarying = timeVarying;
        }

        @Override
        public String toString() {
            return metricStr;
        }

        private static final String BLOCK_METRIC_TYPE_RE;
        static {
            StringBuilder sb = new StringBuilder();
            for (BlockMetricType bmt : values()) {
                if (sb.length() > 0)
                    sb.append("|");
                sb.append(bmt);
            }
            BLOCK_METRIC_TYPE_RE = sb.toString();
        }
    };

    public static enum StoreMetricType {
        STORE_FILE_COUNT("storeFileCount"), STORE_FILE_INDEX_SIZE("storeFileIndexSizeMB"), STORE_FILE_SIZE_MB(
                "storeFileSizeMB"), STATIC_BLOOM_SIZE_KB("staticBloomSizeKB"), MEMSTORE_SIZE_MB(
                        "memstoreSizeMB"), STATIC_INDEX_SIZE_KB("staticIndexSizeKB"), FLUSH_SIZE("flushSize");

        private final String metricStr;

        StoreMetricType(String metricStr) {
            this.metricStr = metricStr;
        }

        @Override
        public String toString() {
            return metricStr;
        }
    };

    // Constants
    /**
     * A string used when column family or table name is unknown, and in some
     * unit tests. This should not normally show up in metric names but if it
     * does it is better than creating a silent discrepancy in total vs.
     * per-CF/table metrics.
     */
    public static final String UNKNOWN = "__unknown";

    public static final String TABLE_PREFIX = "tbl.";
    public static final String CF_PREFIX = "cf.";
    public static final String BLOCK_TYPE_PREFIX = "bt.";
    public static final String REGION_PREFIX = "region.";

    public static final String CF_UNKNOWN_PREFIX = CF_PREFIX + UNKNOWN + ".";
    public static final String CF_BAD_FAMILY_PREFIX = CF_PREFIX + "__badfamily.";

    /** Use for readability when obtaining non-compaction counters */
    public static final boolean NO_COMPACTION = false;

    public static final String METRIC_GETSIZE = "getsize";
    public static final String METRIC_NEXTSIZE = "nextsize";

    /**
     * A special schema metric value that means "all tables aggregated" or
     * "all column families aggregated" when used as a table name or a column
     * family name.
     */
    public static final String TOTAL_KEY = "";

    /**
     * Special handling for meta-block-specific metrics for
     * backwards-compatibility.
     */
    private static final String META_BLOCK_CATEGORY_STR = "Meta";

    private static final int NUM_BLOCK_CATEGORIES = BlockCategory.values().length;

    private static final int NUM_METRIC_TYPES = BlockMetricType.values().length;

    static final boolean[] BOOL_VALUES = new boolean[] { false, true };

    private static final int NUM_BLOCK_METRICS = NUM_BLOCK_CATEGORIES * // blockCategory
            BOOL_VALUES.length * // isCompaction
            NUM_METRIC_TYPES; // metricType

    private static final int NUM_STORE_METRIC_TYPES = StoreMetricType.values().length;

    /** Conf key controlling whether we include table name in metric names */
    private static final String SHOW_TABLE_NAME_CONF_KEY = "hbase.metrics.showTableName";

    /** We use this when too many column families are involved in a request. */
    private static final String MORE_CFS_OMITTED_STR = "__more";

    /**
     * Maximum length of a metric name prefix. Used when constructing metric
     * names from a set of column families participating in a request.
     */
    private static final int MAX_METRIC_PREFIX_LENGTH = 256 - MORE_CFS_OMITTED_STR.length();

    // Global variables
    /**
     * Maps a string key consisting of table name and column family name, with
     * table name optionally replaced with {@link #TOTAL_KEY} if per-table
     * metrics are disabled, to an instance of this class.
     */
    private static final ConcurrentHashMap<String, SchemaMetrics> tableAndFamilyToMetrics = new ConcurrentHashMap<String, SchemaMetrics>();

    /** Metrics for all tables and column families. */
    // This has to be initialized after cfToMetrics.
    public static final SchemaMetrics ALL_SCHEMA_METRICS = getInstance(TOTAL_KEY, TOTAL_KEY);

    /** Threshold for flush the metrics, currently used only for "on cache hit" */
    private static final long THRESHOLD_METRICS_FLUSH = 100l;

    /**
     * Whether to include table name in metric names. If this is null, it has not
     * been initialized. This is a global instance, but we also have a copy of it
     * per a {@link SchemaMetrics} object to avoid synchronization overhead.
     */
    private static volatile Boolean useTableNameGlobally;

    /** Whether we logged a message about configuration inconsistency */
    private static volatile boolean loggedConfInconsistency;

    // Instance variables
    private final String[] blockMetricNames = new String[NUM_BLOCK_METRICS];
    private final boolean[] blockMetricTimeVarying = new boolean[NUM_BLOCK_METRICS];

    private final String[] bloomMetricNames = new String[2];
    private final String[] storeMetricNames = new String[NUM_STORE_METRIC_TYPES];
    private final String[] storeMetricNamesMax = new String[NUM_STORE_METRIC_TYPES];
    private final AtomicLongArray onHitCacheMetrics = new AtomicLongArray(
            NUM_BLOCK_CATEGORIES * BOOL_VALUES.length);

    private SchemaMetrics(final String tableName, final String cfName) {
        String metricPrefix = SchemaMetrics.generateSchemaMetricsPrefix(tableName, cfName);

        for (BlockCategory blockCategory : BlockCategory.values()) {
            for (boolean isCompaction : BOOL_VALUES) {
                // initialize the cache metrics
                onHitCacheMetrics.set(getCacheHitMetricIndex(blockCategory, isCompaction), 0);

                for (BlockMetricType metricType : BlockMetricType.values()) {
                    if (!metricType.compactionAware && isCompaction) {
                        continue;
                    }

                    StringBuilder sb = new StringBuilder(metricPrefix);
                    if (blockCategory != BlockCategory.ALL_CATEGORIES && blockCategory != BlockCategory.META) {
                        String categoryStr = blockCategory.toString();
                        categoryStr = categoryStr.charAt(0) + categoryStr.substring(1).toLowerCase();
                        sb.append(BLOCK_TYPE_PREFIX + categoryStr + ".");
                    }

                    if (metricType.compactionAware) {
                        sb.append(isCompaction ? "compaction" : "fs");
                    }

                    // A special-case for meta blocks for backwards-compatibility.
                    if (blockCategory == BlockCategory.META) {
                        sb.append(META_BLOCK_CATEGORY_STR);
                    }

                    sb.append(metricType);

                    int i = getBlockMetricIndex(blockCategory, isCompaction, metricType);
                    blockMetricNames[i] = sb.toString().intern();
                    blockMetricTimeVarying[i] = metricType.timeVarying;
                }
            }
        }

        for (boolean isInBloom : BOOL_VALUES) {
            bloomMetricNames[isInBloom ? 1 : 0] = metricPrefix
                    + (isInBloom ? "keyMaybeInBloomCnt" : "keyNotInBloomCnt");
        }

        for (StoreMetricType storeMetric : StoreMetricType.values()) {
            String coreName = metricPrefix + storeMetric.toString();
            storeMetricNames[storeMetric.ordinal()] = coreName;
            storeMetricNamesMax[storeMetric.ordinal()] = coreName + ".max";
        }
    }

    /**
     * Returns a {@link SchemaMetrics} object for the given table and column
     * family, instantiating it if necessary.
     *
     * @param tableName table name (null is interpreted as "unknown"). This is
     *          ignored
     * @param cfName column family name (null is interpreted as "unknown")
     */
    public static SchemaMetrics getInstance(String tableName, String cfName) {
        if (tableName == null) {
            tableName = UNKNOWN;
        }

        if (cfName == null) {
            cfName = UNKNOWN;
        }

        tableName = getEffectiveTableName(tableName);

        final String instanceKey = tableName + "\t" + cfName;
        SchemaMetrics schemaMetrics = tableAndFamilyToMetrics.get(instanceKey);
        if (schemaMetrics != null) {
            return schemaMetrics;
        }

        schemaMetrics = new SchemaMetrics(tableName, cfName);
        SchemaMetrics existingMetrics = tableAndFamilyToMetrics.putIfAbsent(instanceKey, schemaMetrics);
        return existingMetrics != null ? existingMetrics : schemaMetrics;
    }

    private static final int getCacheHitMetricIndex(BlockCategory blockCategory, boolean isCompaction) {
        return blockCategory.ordinal() * BOOL_VALUES.length + (isCompaction ? 1 : 0);
    }

    private static final int getBlockMetricIndex(BlockCategory blockCategory, boolean isCompaction,
            BlockMetricType metricType) {
        int i = 0;
        i = i * NUM_BLOCK_CATEGORIES + blockCategory.ordinal();
        i = i * BOOL_VALUES.length + (isCompaction ? 1 : 0);
        i = i * NUM_METRIC_TYPES + metricType.ordinal();
        return i;
    }

    public String getBlockMetricName(BlockCategory blockCategory, boolean isCompaction,
            BlockMetricType metricType) {
        if (isCompaction && !metricType.compactionAware) {
            throw new IllegalArgumentException("isCompaction cannot be true for " + metricType);
        }
        return blockMetricNames[getBlockMetricIndex(blockCategory, isCompaction, metricType)];
    }

    public String getBloomMetricName(boolean isInBloom) {
        return bloomMetricNames[isInBloom ? 1 : 0];
    }

    /**
     * Increments the given metric, both per-CF and aggregate, for both the given
     * category and all categories in aggregate (four counters total).
     */
    private void incrNumericMetric(BlockCategory blockCategory, boolean isCompaction, BlockMetricType metricType) {
        incrNumericMetric(blockCategory, isCompaction, metricType, 1);
    }

    /**
     * Increments the given metric, both per-CF and aggregate, for both the given
     * category and all categories in aggregate (four counters total).
     */
    private void incrNumericMetric(BlockCategory blockCategory, boolean isCompaction, BlockMetricType metricType,
            long amount) {
        if (blockCategory == null) {
            blockCategory = BlockCategory.UNKNOWN; // So that we see this in stats.
        }
        RegionMetricsStorage.incrNumericMetric(getBlockMetricName(blockCategory, isCompaction, metricType), amount);

        if (blockCategory != BlockCategory.ALL_CATEGORIES) {
            incrNumericMetric(BlockCategory.ALL_CATEGORIES, isCompaction, metricType, amount);
        }
    }

    private void addToReadTime(BlockCategory blockCategory, boolean isCompaction, long timeMs) {
        RegionMetricsStorage.incrTimeVaryingMetric(
                getBlockMetricName(blockCategory, isCompaction, BlockMetricType.READ_TIME), timeMs);

        // Also update the read time aggregated across all block categories
        if (blockCategory != BlockCategory.ALL_CATEGORIES) {
            addToReadTime(BlockCategory.ALL_CATEGORIES, isCompaction, timeMs);
        }
    }

    /**
     * Used to accumulate store metrics across multiple regions in a region
     * server.  These metrics are not "persistent", i.e. we keep overriding them
     * on every update instead of incrementing, so we need to accumulate them in
     * a temporary map before pushing them to the global metric collection.
     * @param tmpMap a temporary map for accumulating store metrics
     * @param storeMetricType the store metric type to increment
     * @param val the value to add to the metric
     */
    public void accumulateStoreMetric(final Map<String, MutableDouble> tmpMap, StoreMetricType storeMetricType,
            double val) {
        final String key = getStoreMetricName(storeMetricType);
        if (tmpMap.get(key) == null) {
            tmpMap.put(key, new MutableDouble(val));
        } else {
            tmpMap.get(key).add(val);
        }

        if (this == ALL_SCHEMA_METRICS) {
            // also compute the max value across all Stores on this server
            final String maxKey = getStoreMetricNameMax(storeMetricType);
            MutableDouble cur = tmpMap.get(maxKey);
            if (cur == null) {
                tmpMap.put(maxKey, new MutableDouble(val));
            } else if (cur.doubleValue() < val) {
                cur.setValue(val);
            }
        } else {
            ALL_SCHEMA_METRICS.accumulateStoreMetric(tmpMap, storeMetricType, val);
        }
    }

    public String getStoreMetricName(StoreMetricType storeMetricType) {
        return storeMetricNames[storeMetricType.ordinal()];
    }

    public String getStoreMetricNameMax(StoreMetricType storeMetricType) {
        return storeMetricNamesMax[storeMetricType.ordinal()];
    }

    /**
     * Update a metric that does not get reset on every poll.
     * @param storeMetricType the store metric to update
     * @param value the value to update the metric to
     */
    public void updatePersistentStoreMetric(StoreMetricType storeMetricType, long value) {
        RegionMetricsStorage.incrNumericPersistentMetric(storeMetricNames[storeMetricType.ordinal()], value);
    }

    /**
     * Updates the number of hits and the total number of block reads on a block
     * cache hit.
     */
    public void updateOnCacheHit(BlockCategory blockCategory, boolean isCompaction) {
        updateOnCacheHit(blockCategory, isCompaction, 1);
    }

    /**
     * Updates the number of hits and the total number of block reads on a block
     * cache hit.
     */
    public void updateOnCacheHit(BlockCategory blockCategory, boolean isCompaction, long count) {
        blockCategory.expectSpecific();
        int idx = getCacheHitMetricIndex(blockCategory, isCompaction);

        if (this.onHitCacheMetrics.addAndGet(idx, count) > THRESHOLD_METRICS_FLUSH) {
            flushCertainOnCacheHitMetrics(blockCategory, isCompaction);
        }

        if (this != ALL_SCHEMA_METRICS) {
            ALL_SCHEMA_METRICS.updateOnCacheHit(blockCategory, isCompaction, count);
        }
    }

    private void flushCertainOnCacheHitMetrics(BlockCategory blockCategory, boolean isCompaction) {
        int idx = getCacheHitMetricIndex(blockCategory, isCompaction);
        long tempCount = this.onHitCacheMetrics.getAndSet(idx, 0);

        if (tempCount > 0) {
            incrNumericMetric(blockCategory, isCompaction, BlockMetricType.CACHE_HIT, tempCount);
            incrNumericMetric(blockCategory, isCompaction, BlockMetricType.READ_COUNT, tempCount);
        }
    }

    /**
     * Flush the on cache hit metrics;
     */
    private void flushOnCacheHitMetrics() {
        for (BlockCategory blockCategory : BlockCategory.values()) {
            for (boolean isCompaction : BOOL_VALUES) {
                flushCertainOnCacheHitMetrics(blockCategory, isCompaction);
            }
        }

        if (this != ALL_SCHEMA_METRICS) {
            ALL_SCHEMA_METRICS.flushOnCacheHitMetrics();
        }
    }

    /**
     * Notify the SchemaMetrics to flush all of the the metrics
     */
    public void flushMetrics() {
        // currently only for "on cache hit metrics"
        flushOnCacheHitMetrics();
    }

    /**
     * Updates read time, the number of misses, and the total number of block
     * reads on a block cache miss.
     */
    public void updateOnCacheMiss(BlockCategory blockCategory, boolean isCompaction, long timeMs) {
        blockCategory.expectSpecific();
        addToReadTime(blockCategory, isCompaction, timeMs);
        incrNumericMetric(blockCategory, isCompaction, BlockMetricType.CACHE_MISS);
        incrNumericMetric(blockCategory, isCompaction, BlockMetricType.READ_COUNT);
        if (this != ALL_SCHEMA_METRICS) {
            ALL_SCHEMA_METRICS.updateOnCacheMiss(blockCategory, isCompaction, timeMs);
        }
    }

    /**
     * Adds the given delta to the cache size for the given block category and
     * the aggregate metric for all block categories. Updates both the per-CF
     * counter and the counter for all CFs (four metrics total). The cache size
     * metric is "persistent", i.e. it does not get reset when metrics are
     * collected.
     */
    public void addToCacheSize(BlockCategory category, long cacheSizeDelta) {
        if (category == null) {
            category = BlockCategory.ALL_CATEGORIES;
        }
        RegionMetricsStorage.incrNumericPersistentMetric(
                getBlockMetricName(category, false, BlockMetricType.CACHE_SIZE), cacheSizeDelta);

        if (category != BlockCategory.ALL_CATEGORIES) {
            addToCacheSize(BlockCategory.ALL_CATEGORIES, cacheSizeDelta);
        }
    }

    public void updateOnCachePutOrEvict(BlockCategory blockCategory, long cacheSizeDelta, boolean isEviction) {
        addToCacheSize(blockCategory, cacheSizeDelta);
        incrNumericMetric(blockCategory, false, isEviction ? BlockMetricType.EVICTED : BlockMetricType.CACHED);
        if (this != ALL_SCHEMA_METRICS) {
            ALL_SCHEMA_METRICS.updateOnCachePutOrEvict(blockCategory, cacheSizeDelta, isEviction);
        }
    }

    /**
     * Increments both the per-CF and the aggregate counter of bloom
     * positives/negatives as specified by the argument.
     */
    public void updateBloomMetrics(boolean isInBloom) {
        RegionMetricsStorage.incrNumericMetric(getBloomMetricName(isInBloom), 1);
        if (this != ALL_SCHEMA_METRICS) {
            ALL_SCHEMA_METRICS.updateBloomMetrics(isInBloom);
        }
    }

    /**
     * Sets the flag whether to use table name in metric names according to the
     * given configuration. This must be called at least once before
     * instantiating HFile readers/writers.
     */
    public static void configureGlobally(Configuration conf) {
        if (conf != null) {
            final boolean useTableNameNew = conf.getBoolean(SHOW_TABLE_NAME_CONF_KEY, false);
            setUseTableName(useTableNameNew);
        } else {
            setUseTableName(false);
        }
    }

    /**
     * Determine the table name to be included in metric keys. If the global
     * configuration says that we should not use table names in metrics,
     * we always return {@link #TOTAL_KEY} even if nontrivial table name is
     * provided.
     *
     * @param tableName a table name or {@link #TOTAL_KEY} when aggregating
     * across all tables
     * @return the table name to use in metric keys
     */
    private static String getEffectiveTableName(String tableName) {
        if (!tableName.equals(TOTAL_KEY)) {
            // We are provided with a non-trivial table name (including "unknown").
            // We need to know whether table name should be included into metrics.
            if (useTableNameGlobally == null) {
                throw new IllegalStateException("The value of the " + SHOW_TABLE_NAME_CONF_KEY
                        + " conf option has not been specified " + "in SchemaMetrics");
            }
            final boolean useTableName = useTableNameGlobally;
            if (!useTableName) {
                // Don't include table name in metric keys.
                tableName = TOTAL_KEY;
            }
        }
        return tableName;
    }

    /**
     * Method to transform a combination of a table name and a column family name
     * into a metric key prefix. Tables/column family names equal to
     * {@link #TOTAL_KEY} are omitted from the prefix.
     *
     * @param tableName the table name or {@link #TOTAL_KEY} for all tables
     * @param cfName the column family name or {@link #TOTAL_KEY} for all CFs
     * @return the metric name prefix, ending with a dot.
     */
    public static String generateSchemaMetricsPrefix(String tableName, final String cfName) {
        tableName = getEffectiveTableName(tableName);
        String schemaMetricPrefix = tableName.equals(TOTAL_KEY) ? "" : TABLE_PREFIX + tableName + ".";
        schemaMetricPrefix += cfName.equals(TOTAL_KEY) ? "" : CF_PREFIX + cfName + ".";
        return schemaMetricPrefix;
    }

    public static String generateSchemaMetricsPrefix(byte[] tableName, byte[] cfName) {
        return generateSchemaMetricsPrefix(Bytes.toString(tableName), Bytes.toString(cfName));
    }

    /**
     * Method to transform a set of column families in byte[] format with table
     * name into a metric key prefix.
     *
     * @param tableName the table name or {@link #TOTAL_KEY} for all tables
     * @param families the ordered set of column families
     * @return the metric name prefix, ending with a dot, or an empty string in
     *         case of invalid arguments. This is OK since we always expect
     *         some CFs to be included.
     */
    public static String generateSchemaMetricsPrefix(String tableName, Set<byte[]> families) {
        if (families == null || families.isEmpty() || tableName == null || tableName.isEmpty()) {
            return "";
        }

        if (families.size() == 1) {
            return generateSchemaMetricsPrefix(tableName, Bytes.toString(families.iterator().next()));
        }

        tableName = getEffectiveTableName(tableName);
        List<byte[]> sortedFamilies = new ArrayList<byte[]>(families);
        Collections.sort(sortedFamilies, Bytes.BYTES_COMPARATOR);

        StringBuilder sb = new StringBuilder();

        int numCFsLeft = families.size();
        for (byte[] family : sortedFamilies) {
            if (sb.length() > MAX_METRIC_PREFIX_LENGTH) {
                sb.append(MORE_CFS_OMITTED_STR);
                break;
            }
            --numCFsLeft;
            sb.append(Bytes.toString(family));
            if (numCFsLeft > 0) {
                sb.append("~");
            }
        }

        return SchemaMetrics.generateSchemaMetricsPrefix(tableName, sb.toString());
    }

    /**
     * Get the prefix for metrics generated about a single region.
     * 
     * @param tableName
     *          the table name or {@link #TOTAL_KEY} for all tables
     * @param regionName
     *          regionName
     * @return the prefix for this table/region combination.
     */
    static String generateRegionMetricsPrefix(String tableName, String regionName) {
        tableName = getEffectiveTableName(tableName);
        String schemaMetricPrefix = tableName.equals(TOTAL_KEY) ? "" : TABLE_PREFIX + tableName + ".";
        schemaMetricPrefix += regionName.equals(TOTAL_KEY) ? "" : REGION_PREFIX + regionName + ".";

        return schemaMetricPrefix;
    }

    /**
     * Sets the flag of whether to use table name in metric names. This flag
     * is specified in configuration and is not expected to change at runtime,
     * so we log an error message when it does change.
     */
    private static void setUseTableName(final boolean useTableNameNew) {
        if (useTableNameGlobally == null) {
            // This configuration option has not yet been set.
            useTableNameGlobally = useTableNameNew;
        } else if (useTableNameGlobally != useTableNameNew && !loggedConfInconsistency) {
            // The configuration is inconsistent and we have not reported it
            // previously. Once we report it, just keep ignoring the new setting.
            LOG.error("Inconsistent configuration. Previous configuration " + "for using table name in metrics: "
                    + useTableNameGlobally + ", " + "new configuration: " + useTableNameNew);
            loggedConfInconsistency = true;
        }
    }

    // Methods used in testing

    private static final String regexEscape(String s) {
        return s.replace(".", "\\.");
    }

    /**
     * Assume that table names used in tests don't contain dots, except for the
     * META table.
     */
    private static final String WORD_AND_DOT_RE_STR = "([^.]+|"
            + regexEscape(Bytes.toString(HConstants.META_TABLE_NAME)) + ")\\.";

    /** "tbl.<table_name>." */
    private static final String TABLE_NAME_RE_STR = "\\b" + regexEscape(TABLE_PREFIX) + WORD_AND_DOT_RE_STR;

    /** "cf.<cf_name>." */
    private static final String CF_NAME_RE_STR = "\\b" + regexEscape(CF_PREFIX) + WORD_AND_DOT_RE_STR;
    private static final Pattern CF_NAME_RE = Pattern.compile(CF_NAME_RE_STR);

    /** "tbl.<table_name>.cf.<cf_name>." */
    private static final Pattern TABLE_AND_CF_NAME_RE = Pattern.compile(TABLE_NAME_RE_STR + CF_NAME_RE_STR);

    private static final Pattern BLOCK_CATEGORY_RE = Pattern
            .compile("\\b" + regexEscape(BLOCK_TYPE_PREFIX) + "[^.]+\\." +
    // Also remove the special-case block type marker for meta blocks
                    "|" + META_BLOCK_CATEGORY_STR + "(?=" + BlockMetricType.BLOCK_METRIC_TYPE_RE + ")");

    /**
     * A suffix for the "number of operations" part of "time-varying metrics". We
     * only use this for metric verification in unit testing. Time-varying
     * metrics are handled by a different code path in production.
     */
    private static String NUM_OPS_SUFFIX = "numops";

    /**
     * A custom suffix that we use for verifying the second component of
     * a "time-varying metric".
     */
    private static String TOTAL_SUFFIX = "_total";
    private static final Pattern TIME_VARYING_SUFFIX_RE = Pattern
            .compile("(" + NUM_OPS_SUFFIX + "|" + TOTAL_SUFFIX + ")$");

    void printMetricNames() {
        for (BlockCategory blockCategory : BlockCategory.values()) {
            for (boolean isCompaction : BOOL_VALUES) {
                for (BlockMetricType metricType : BlockMetricType.values()) {
                    int i = getBlockMetricIndex(blockCategory, isCompaction, metricType);
                    LOG.debug("blockCategory=" + blockCategory + ", " + "metricType=" + metricType
                            + ", isCompaction=" + isCompaction + ", metricName=" + blockMetricNames[i]);
                }
            }
        }
    }

    private Collection<String> getAllMetricNames() {
        List<String> allMetricNames = new ArrayList<String>();
        for (int i = 0; i < blockMetricNames.length; ++i) {
            final String blockMetricName = blockMetricNames[i];
            final boolean timeVarying = blockMetricTimeVarying[i];
            if (blockMetricName != null) {
                if (timeVarying) {
                    allMetricNames.add(blockMetricName + NUM_OPS_SUFFIX);
                    allMetricNames.add(blockMetricName + TOTAL_SUFFIX);
                } else {
                    allMetricNames.add(blockMetricName);
                }
            }
        }
        allMetricNames.addAll(Arrays.asList(bloomMetricNames));
        return allMetricNames;
    }

    private static final boolean isTimeVaryingKey(String metricKey) {
        return metricKey.endsWith(NUM_OPS_SUFFIX) || metricKey.endsWith(TOTAL_SUFFIX);
    }

    private static final String stripTimeVaryingSuffix(String metricKey) {
        return TIME_VARYING_SUFFIX_RE.matcher(metricKey).replaceAll("");
    }

    public static Map<String, Long> getMetricsSnapshot() {
        Map<String, Long> metricsSnapshot = new TreeMap<String, Long>();
        for (SchemaMetrics cfm : tableAndFamilyToMetrics.values()) {
            cfm.flushMetrics();
            for (String metricName : cfm.getAllMetricNames()) {
                long metricValue;
                if (isTimeVaryingKey(metricName)) {
                    Pair<Long, Integer> totalAndCount = RegionMetricsStorage
                            .getTimeVaryingMetric(stripTimeVaryingSuffix(metricName));
                    metricValue = metricName.endsWith(TOTAL_SUFFIX) ? totalAndCount.getFirst()
                            : totalAndCount.getSecond();
                } else {
                    metricValue = RegionMetricsStorage.getNumericMetric(metricName);
                }

                metricsSnapshot.put(metricName, metricValue);
            }
        }
        return metricsSnapshot;
    }

    public static long getLong(Map<String, Long> m, String k) {
        Long l = m.get(k);
        return l != null ? l : 0;
    }

    private static void putLong(Map<String, Long> m, String k, long v) {
        if (v != 0) {
            m.put(k, v);
        } else {
            m.remove(k);
        }
    }

    /**
     * @return the difference between two sets of metrics (second minus first).
     *         Only includes keys that have nonzero difference.
     */
    public static Map<String, Long> diffMetrics(Map<String, Long> a, Map<String, Long> b) {
        Set<String> allKeys = new TreeSet<String>(a.keySet());
        allKeys.addAll(b.keySet());
        Map<String, Long> diff = new TreeMap<String, Long>();
        for (String k : allKeys) {
            long aVal = getLong(a, k);
            long bVal = getLong(b, k);
            if (aVal != bVal) {
                diff.put(k, bVal - aVal);
            }
        }
        return diff;
    }

    public static void validateMetricChanges(Map<String, Long> oldMetrics) {
        final Map<String, Long> newMetrics = getMetricsSnapshot();
        final Map<String, Long> allCfDeltas = new TreeMap<String, Long>();
        final Map<String, Long> allBlockCategoryDeltas = new TreeMap<String, Long>();
        final Map<String, Long> deltas = diffMetrics(oldMetrics, newMetrics);
        final Pattern cfTableMetricRE = useTableNameGlobally ? TABLE_AND_CF_NAME_RE : CF_NAME_RE;
        final Set<String> allKeys = new TreeSet<String>(oldMetrics.keySet());
        allKeys.addAll(newMetrics.keySet());

        for (SchemaMetrics cfm : tableAndFamilyToMetrics.values()) {
            for (String metricName : cfm.getAllMetricNames()) {
                if (metricName.startsWith(CF_PREFIX + CF_PREFIX)) {
                    throw new AssertionError("Column family prefix used twice: " + metricName);
                }

                final long oldValue = getLong(oldMetrics, metricName);
                final long newValue = getLong(newMetrics, metricName);
                final long delta = newValue - oldValue;

                // Re-calculate values of metrics with no column family (or CF/table)
                // specified based on all metrics with CF (or CF/table) specified.
                if (delta != 0) {
                    if (cfm != ALL_SCHEMA_METRICS) {
                        final String aggregateMetricName = cfTableMetricRE.matcher(metricName).replaceAll("");
                        if (!aggregateMetricName.equals(metricName)) {
                            LOG.debug("Counting " + delta + " units of " + metricName + " towards "
                                    + aggregateMetricName);

                            putLong(allCfDeltas, aggregateMetricName,
                                    getLong(allCfDeltas, aggregateMetricName) + delta);
                        }
                    } else {
                        LOG.debug("Metric=" + metricName + ", delta=" + delta);
                    }
                }

                Matcher matcher = BLOCK_CATEGORY_RE.matcher(metricName);
                if (matcher.find()) {
                    // Only process per-block-category metrics
                    String metricNoBlockCategory = matcher.replaceAll("");

                    putLong(allBlockCategoryDeltas, metricNoBlockCategory,
                            getLong(allBlockCategoryDeltas, metricNoBlockCategory) + delta);
                }
            }
        }

        StringBuilder errors = new StringBuilder();
        for (String key : ALL_SCHEMA_METRICS.getAllMetricNames()) {
            long actual = getLong(deltas, key);
            long expected = getLong(allCfDeltas, key);
            if (actual != expected) {
                if (errors.length() > 0)
                    errors.append("\n");
                errors.append("The all-CF metric " + key + " changed by " + actual
                        + " but the aggregation of per-CF/table metrics " + "yields " + expected);
            }
        }

        // Verify metrics computed for all block types based on the aggregation
        // of per-block-type metrics.
        for (String key : allKeys) {
            if (BLOCK_CATEGORY_RE.matcher(key).find() || key.contains(ALL_SCHEMA_METRICS.getBloomMetricName(false))
                    || key.contains(ALL_SCHEMA_METRICS.getBloomMetricName(true))) {
                // Skip per-block-category metrics. Also skip bloom filters, because
                // they are not aggregated per block type.
                continue;
            }
            long actual = getLong(deltas, key);
            long expected = getLong(allBlockCategoryDeltas, key);
            if (actual != expected) {
                if (errors.length() > 0)
                    errors.append("\n");
                errors.append("The all-block-category metric " + key + " changed by " + actual
                        + " but the aggregation of " + "per-block-category metrics yields " + expected);
            }
        }

        if (errors.length() > 0) {
            throw new AssertionError(errors.toString());
        }
    }

    /**
     * Creates an instance pretending both the table and column family are
     * unknown. Used in unit tests.
     */
    public static SchemaMetrics getUnknownInstanceForTest() {
        return getInstance(UNKNOWN, UNKNOWN);
    }

    /**
     * Set the flag to use or not use table name in metric names. Used in unit
     * tests, so the flag can be set arbitrarily.
     */
    public static void setUseTableNameInTest(final boolean useTableNameNew) {
        useTableNameGlobally = useTableNameNew;
    }

    /** Formats the given map of metrics in a human-readable way. */
    public static String formatMetrics(Map<String, Long> metrics) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Long> entry : metrics.entrySet()) {
            if (sb.length() > 0) {
                sb.append('\n');
            }
            sb.append(entry.getKey() + " : " + entry.getValue());
        }
        return sb.toString();
    }

}